mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-03-28 13:01:57 +00:00
move classes to portable
This commit is contained in:
@@ -1,107 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Logging;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
|
||||
{
|
||||
public class DirectRecorder : IRecorder
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
|
||||
public DirectRecorder(ILogger logger, IHttpClient httpClient, IFileSystem fileSystem)
|
||||
{
|
||||
_logger = logger;
|
||||
_httpClient = httpClient;
|
||||
_fileSystem = fileSystem;
|
||||
}
|
||||
|
||||
public string GetOutputPath(MediaSourceInfo mediaSource, string targetFile)
|
||||
{
|
||||
return targetFile;
|
||||
}
|
||||
|
||||
public async Task Record(MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
|
||||
{
|
||||
var httpRequestOptions = new HttpRequestOptions()
|
||||
{
|
||||
Url = mediaSource.Path
|
||||
};
|
||||
|
||||
httpRequestOptions.BufferContent = false;
|
||||
|
||||
using (var response = await _httpClient.SendAsync(httpRequestOptions, "GET").ConfigureAwait(false))
|
||||
{
|
||||
_logger.Info("Opened recording stream from tuner provider");
|
||||
|
||||
using (var output = _fileSystem.GetFileStream(targetFile, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read))
|
||||
{
|
||||
onStarted();
|
||||
|
||||
_logger.Info("Copying recording stream to file {0}", targetFile);
|
||||
|
||||
// The media source if infinite so we need to handle stopping ourselves
|
||||
var durationToken = new CancellationTokenSource(duration);
|
||||
cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token;
|
||||
|
||||
await CopyUntilCancelled(response.Content, output, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.Info("Recording completed to file {0}", targetFile);
|
||||
}
|
||||
|
||||
private const int BufferSize = 81920;
|
||||
public static Task CopyUntilCancelled(Stream source, Stream target, CancellationToken cancellationToken)
|
||||
{
|
||||
return CopyUntilCancelled(source, target, null, cancellationToken);
|
||||
}
|
||||
public static async Task CopyUntilCancelled(Stream source, Stream target, Action onStarted, CancellationToken cancellationToken)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
var bytesRead = await CopyToAsyncInternal(source, target, BufferSize, onStarted, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
onStarted = null;
|
||||
|
||||
//var position = fs.Position;
|
||||
//_logger.Debug("Streamed {0} bytes to position {1} from file {2}", bytesRead, position, path);
|
||||
|
||||
if (bytesRead == 0)
|
||||
{
|
||||
await Task.Delay(100).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<int> CopyToAsyncInternal(Stream source, Stream destination, Int32 bufferSize, Action onStarted, CancellationToken cancellationToken)
|
||||
{
|
||||
byte[] buffer = new byte[bufferSize];
|
||||
int bytesRead;
|
||||
int totalBytesRead = 0;
|
||||
|
||||
while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0)
|
||||
{
|
||||
await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
totalBytesRead += bytesRead;
|
||||
|
||||
if (onStarted != null)
|
||||
{
|
||||
onStarted();
|
||||
}
|
||||
onStarted = null;
|
||||
}
|
||||
|
||||
return totalBytesRead;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,36 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Security;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
|
||||
{
|
||||
public class EmbyTVRegistration : IRequiresRegistration
|
||||
{
|
||||
private readonly ISecurityManager _securityManager;
|
||||
|
||||
public static EmbyTVRegistration Instance;
|
||||
|
||||
public EmbyTVRegistration(ISecurityManager securityManager)
|
||||
{
|
||||
_securityManager = securityManager;
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
private bool? _isXmlTvEnabled;
|
||||
|
||||
public Task LoadRegistrationInfoAsync()
|
||||
{
|
||||
_isXmlTvEnabled = null;
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
public async Task<bool> EnableXmlTv()
|
||||
{
|
||||
if (!_isXmlTvEnabled.HasValue)
|
||||
{
|
||||
var info = await _securityManager.GetRegistrationStatus("xmltv").ConfigureAwait(false);
|
||||
_isXmlTvEnabled = info.IsValid;
|
||||
}
|
||||
return _isXmlTvEnabled.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,326 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.LiveTv;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
|
||||
{
|
||||
public class EncodedRecorder : IRecorder
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
private readonly IServerApplicationPaths _appPaths;
|
||||
private readonly LiveTvOptions _liveTvOptions;
|
||||
private bool _hasExited;
|
||||
private Stream _logFileStream;
|
||||
private string _targetPath;
|
||||
private Process _process;
|
||||
private readonly IJsonSerializer _json;
|
||||
private readonly TaskCompletionSource<bool> _taskCompletionSource = new TaskCompletionSource<bool>();
|
||||
|
||||
public EncodedRecorder(ILogger logger, IFileSystem fileSystem, IMediaEncoder mediaEncoder, IServerApplicationPaths appPaths, IJsonSerializer json, LiveTvOptions liveTvOptions, IHttpClient httpClient)
|
||||
{
|
||||
_logger = logger;
|
||||
_fileSystem = fileSystem;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
_appPaths = appPaths;
|
||||
_json = json;
|
||||
_liveTvOptions = liveTvOptions;
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
private string OutputFormat
|
||||
{
|
||||
get
|
||||
{
|
||||
var format = _liveTvOptions.RecordingEncodingFormat;
|
||||
|
||||
if (string.Equals(format, "mkv", StringComparison.OrdinalIgnoreCase) || string.Equals(_liveTvOptions.RecordedVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "mkv";
|
||||
}
|
||||
|
||||
return "mp4";
|
||||
}
|
||||
}
|
||||
|
||||
public string GetOutputPath(MediaSourceInfo mediaSource, string targetFile)
|
||||
{
|
||||
return Path.ChangeExtension(targetFile, "." + OutputFormat);
|
||||
}
|
||||
|
||||
public async Task Record(MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
|
||||
{
|
||||
var durationToken = new CancellationTokenSource(duration);
|
||||
cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token;
|
||||
|
||||
await RecordFromFile(mediaSource, mediaSource.Path, targetFile, duration, onStarted, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
_logger.Info("Recording completed to file {0}", targetFile);
|
||||
}
|
||||
|
||||
private Task RecordFromFile(MediaSourceInfo mediaSource, string inputFile, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
|
||||
{
|
||||
_targetPath = targetFile;
|
||||
_fileSystem.CreateDirectory(Path.GetDirectoryName(targetFile));
|
||||
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
CreateNoWindow = true,
|
||||
UseShellExecute = false,
|
||||
|
||||
// Must consume both stdout and stderr or deadlocks may occur
|
||||
//RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
RedirectStandardInput = true,
|
||||
|
||||
FileName = _mediaEncoder.EncoderPath,
|
||||
Arguments = GetCommandLineArgs(mediaSource, inputFile, targetFile, duration),
|
||||
|
||||
WindowStyle = ProcessWindowStyle.Hidden,
|
||||
ErrorDialog = false
|
||||
},
|
||||
|
||||
EnableRaisingEvents = true
|
||||
};
|
||||
|
||||
_process = process;
|
||||
|
||||
var commandLineLogMessage = process.StartInfo.FileName + " " + process.StartInfo.Arguments;
|
||||
_logger.Info(commandLineLogMessage);
|
||||
|
||||
var logFilePath = Path.Combine(_appPaths.LogDirectoryPath, "record-transcode-" + Guid.NewGuid() + ".txt");
|
||||
_fileSystem.CreateDirectory(Path.GetDirectoryName(logFilePath));
|
||||
|
||||
// FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
|
||||
_logFileStream = _fileSystem.GetFileStream(logFilePath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true);
|
||||
|
||||
var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(_json.SerializeToString(mediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine);
|
||||
_logFileStream.Write(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length);
|
||||
|
||||
process.Exited += (sender, args) => OnFfMpegProcessExited(process, inputFile);
|
||||
|
||||
process.Start();
|
||||
|
||||
cancellationToken.Register(Stop);
|
||||
|
||||
// MUST read both stdout and stderr asynchronously or a deadlock may occurr
|
||||
//process.BeginOutputReadLine();
|
||||
|
||||
onStarted();
|
||||
|
||||
// Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback
|
||||
StartStreamingLog(process.StandardError.BaseStream, _logFileStream);
|
||||
|
||||
_logger.Info("ffmpeg recording process started for {0}", _targetPath);
|
||||
|
||||
return _taskCompletionSource.Task;
|
||||
}
|
||||
|
||||
private string GetCommandLineArgs(MediaSourceInfo mediaSource, string inputTempFile, string targetFile, TimeSpan duration)
|
||||
{
|
||||
string videoArgs;
|
||||
if (EncodeVideo(mediaSource))
|
||||
{
|
||||
var maxBitrate = 25000000;
|
||||
videoArgs = string.Format(
|
||||
"-codec:v:0 libx264 -force_key_frames \"expr:gte(t,n_forced*5)\" {0} -pix_fmt yuv420p -preset superfast -crf 23 -b:v {1} -maxrate {1} -bufsize ({1}*2) -vsync -1 -profile:v high -level 41",
|
||||
GetOutputSizeParam(),
|
||||
maxBitrate.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
else
|
||||
{
|
||||
videoArgs = "-codec:v:0 copy";
|
||||
}
|
||||
|
||||
var durationParam = " -t " + _mediaEncoder.GetTimeParameter(duration.Ticks);
|
||||
var inputModifiers = "-fflags +genpts -async 1 -vsync -1";
|
||||
var commandLineArgs = "-i \"{0}\"{4} -sn {2} -map_metadata -1 -threads 0 {3} -y \"{1}\"";
|
||||
|
||||
long startTimeTicks = 0;
|
||||
//if (mediaSource.DateLiveStreamOpened.HasValue)
|
||||
//{
|
||||
// var elapsed = DateTime.UtcNow - mediaSource.DateLiveStreamOpened.Value;
|
||||
// elapsed -= TimeSpan.FromSeconds(10);
|
||||
// if (elapsed.TotalSeconds >= 0)
|
||||
// {
|
||||
// startTimeTicks = elapsed.Ticks + startTimeTicks;
|
||||
// }
|
||||
//}
|
||||
|
||||
if (mediaSource.ReadAtNativeFramerate)
|
||||
{
|
||||
inputModifiers += " -re";
|
||||
}
|
||||
|
||||
if (startTimeTicks > 0)
|
||||
{
|
||||
inputModifiers = "-ss " + _mediaEncoder.GetTimeParameter(startTimeTicks) + " " + inputModifiers;
|
||||
}
|
||||
|
||||
commandLineArgs = string.Format(commandLineArgs, inputTempFile, targetFile, videoArgs, GetAudioArgs(mediaSource), durationParam);
|
||||
|
||||
return inputModifiers + " " + commandLineArgs;
|
||||
}
|
||||
|
||||
private string GetAudioArgs(MediaSourceInfo mediaSource)
|
||||
{
|
||||
var mediaStreams = mediaSource.MediaStreams ?? new List<MediaStream>();
|
||||
var inputAudioCodec = mediaStreams.Where(i => i.Type == MediaStreamType.Audio).Select(i => i.Codec).FirstOrDefault() ?? string.Empty;
|
||||
|
||||
// do not copy aac because many players have difficulty with aac_latm
|
||||
if (_liveTvOptions.EnableOriginalAudioWithEncodedRecordings && !string.Equals(inputAudioCodec, "aac", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "-codec:a:0 copy";
|
||||
}
|
||||
|
||||
var audioChannels = 2;
|
||||
var audioStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio);
|
||||
if (audioStream != null)
|
||||
{
|
||||
audioChannels = audioStream.Channels ?? audioChannels;
|
||||
}
|
||||
return "-codec:a:0 aac -strict experimental -ab 320000";
|
||||
}
|
||||
|
||||
private bool EncodeVideo(MediaSourceInfo mediaSource)
|
||||
{
|
||||
if (string.Equals(_liveTvOptions.RecordedVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var mediaStreams = mediaSource.MediaStreams ?? new List<MediaStream>();
|
||||
return !mediaStreams.Any(i => i.Type == MediaStreamType.Video && string.Equals(i.Codec, "h264", StringComparison.OrdinalIgnoreCase) && !i.IsInterlaced);
|
||||
}
|
||||
|
||||
protected string GetOutputSizeParam()
|
||||
{
|
||||
var filters = new List<string>();
|
||||
|
||||
filters.Add("yadif=0:-1:0");
|
||||
|
||||
var output = string.Empty;
|
||||
|
||||
if (filters.Count > 0)
|
||||
{
|
||||
output += string.Format(" -vf \"{0}\"", string.Join(",", filters.ToArray()));
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private void Stop()
|
||||
{
|
||||
if (!_hasExited)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.Info("Killing ffmpeg recording process for {0}", _targetPath);
|
||||
|
||||
//process.Kill();
|
||||
_process.StandardInput.WriteLine("q");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error killing transcoding job for {0}", ex, _targetPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes the exited.
|
||||
/// </summary>
|
||||
private void OnFfMpegProcessExited(Process process, string inputFile)
|
||||
{
|
||||
_hasExited = true;
|
||||
|
||||
DisposeLogStream();
|
||||
|
||||
try
|
||||
{
|
||||
var exitCode = process.ExitCode;
|
||||
|
||||
_logger.Info("FFMpeg recording exited with code {0} for {1}", exitCode, _targetPath);
|
||||
|
||||
if (exitCode == 0)
|
||||
{
|
||||
_taskCompletionSource.TrySetResult(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
_taskCompletionSource.TrySetException(new Exception(string.Format("Recording for {0} failed. Exit code {1}", _targetPath, exitCode)));
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
_logger.Error("FFMpeg recording exited with an error for {0}.", _targetPath);
|
||||
_taskCompletionSource.TrySetException(new Exception(string.Format("Recording for {0} failed", _targetPath)));
|
||||
}
|
||||
}
|
||||
|
||||
private void DisposeLogStream()
|
||||
{
|
||||
if (_logFileStream != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logFileStream.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error disposing recording log stream", ex);
|
||||
}
|
||||
|
||||
_logFileStream = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async void StartStreamingLog(Stream source, Stream target)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var reader = new StreamReader(source))
|
||||
{
|
||||
while (!reader.EndOfStream)
|
||||
{
|
||||
var line = await reader.ReadLineAsync().ConfigureAwait(false);
|
||||
|
||||
var bytes = Encoding.UTF8.GetBytes(Environment.NewLine + line);
|
||||
|
||||
await target.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
|
||||
await target.FlushAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// Don't spam the log. This doesn't seem to throw in windows, but sometimes under linux
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error reading ffmpeg recording log", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
using MediaBrowser.Controller.Plugins;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
|
||||
{
|
||||
public class EntryPoint : IServerEntryPoint
|
||||
{
|
||||
public void Run()
|
||||
{
|
||||
EmbyTV.Current.Start();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Model.Dto;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
|
||||
{
|
||||
public interface IRecorder
|
||||
{
|
||||
/// <summary>
|
||||
/// Records the specified media source.
|
||||
/// </summary>
|
||||
/// <param name="mediaSource">The media source.</param>
|
||||
/// <param name="targetFile">The target file.</param>
|
||||
/// <param name="duration">The duration.</param>
|
||||
/// <param name="onStarted">The on started.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
Task Record(MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken);
|
||||
|
||||
string GetOutputPath(MediaSourceInfo mediaSource, string targetFile);
|
||||
}
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
using MediaBrowser.Model.Logging;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Model.IO;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
|
||||
{
|
||||
public class ItemDataProvider<T>
|
||||
where T : class
|
||||
{
|
||||
private readonly object _fileDataLock = new object();
|
||||
private List<T> _items;
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
protected readonly ILogger Logger;
|
||||
private readonly string _dataPath;
|
||||
protected readonly Func<T, T, bool> EqualityComparer;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
|
||||
public ItemDataProvider(IFileSystem fileSystem, IJsonSerializer jsonSerializer, ILogger logger, string dataPath, Func<T, T, bool> equalityComparer)
|
||||
{
|
||||
Logger = logger;
|
||||
_dataPath = dataPath;
|
||||
EqualityComparer = equalityComparer;
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_fileSystem = fileSystem;
|
||||
}
|
||||
|
||||
public IReadOnlyList<T> GetAll()
|
||||
{
|
||||
lock (_fileDataLock)
|
||||
{
|
||||
if (_items == null)
|
||||
{
|
||||
Logger.Info("Loading live tv data from {0}", _dataPath);
|
||||
_items = GetItemsFromFile(_dataPath);
|
||||
}
|
||||
return _items.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
private List<T> GetItemsFromFile(string path)
|
||||
{
|
||||
var jsonFile = path + ".json";
|
||||
|
||||
try
|
||||
{
|
||||
return _jsonSerializer.DeserializeFromFile<List<T>>(jsonFile) ?? new List<T>();
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
}
|
||||
catch (DirectoryNotFoundException)
|
||||
{
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
Logger.ErrorException("Error deserializing {0}", ex, jsonFile);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.ErrorException("Error deserializing {0}", ex, jsonFile);
|
||||
}
|
||||
return new List<T>();
|
||||
}
|
||||
|
||||
private void UpdateList(List<T> newList)
|
||||
{
|
||||
if (newList == null)
|
||||
{
|
||||
throw new ArgumentNullException("newList");
|
||||
}
|
||||
|
||||
var file = _dataPath + ".json";
|
||||
_fileSystem.CreateDirectory(Path.GetDirectoryName(file));
|
||||
|
||||
lock (_fileDataLock)
|
||||
{
|
||||
_jsonSerializer.SerializeToFile(newList, file);
|
||||
_items = newList;
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void Update(T item)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
throw new ArgumentNullException("item");
|
||||
}
|
||||
|
||||
var list = GetAll().ToList();
|
||||
|
||||
var index = list.FindIndex(i => EqualityComparer(i, item));
|
||||
|
||||
if (index == -1)
|
||||
{
|
||||
throw new ArgumentException("item not found");
|
||||
}
|
||||
|
||||
list[index] = item;
|
||||
|
||||
UpdateList(list);
|
||||
}
|
||||
|
||||
public virtual void Add(T item)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
throw new ArgumentNullException("item");
|
||||
}
|
||||
|
||||
var list = GetAll().ToList();
|
||||
|
||||
if (list.Any(i => EqualityComparer(i, item)))
|
||||
{
|
||||
throw new ArgumentException("item already exists");
|
||||
}
|
||||
|
||||
list.Add(item);
|
||||
|
||||
UpdateList(list);
|
||||
}
|
||||
|
||||
public void AddOrUpdate(T item)
|
||||
{
|
||||
var list = GetAll().ToList();
|
||||
|
||||
if (!list.Any(i => EqualityComparer(i, item)))
|
||||
{
|
||||
Add(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
Update(item);
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void Delete(T item)
|
||||
{
|
||||
var list = GetAll().Where(i => !EqualityComparer(i, item)).ToList();
|
||||
|
||||
UpdateList(list);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using MediaBrowser.Model.LiveTv;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
|
||||
{
|
||||
internal class RecordingHelper
|
||||
{
|
||||
public static DateTime GetStartTime(TimerInfo timer)
|
||||
{
|
||||
return timer.StartDate.AddSeconds(-timer.PrePaddingSeconds);
|
||||
}
|
||||
|
||||
public static TimerInfo CreateTimer(ProgramInfo parent, SeriesTimerInfo seriesTimer)
|
||||
{
|
||||
var timer = new TimerInfo();
|
||||
|
||||
timer.ChannelId = parent.ChannelId;
|
||||
timer.Id = (seriesTimer.Id + parent.Id).GetMD5().ToString("N");
|
||||
timer.StartDate = parent.StartDate;
|
||||
timer.EndDate = parent.EndDate;
|
||||
timer.ProgramId = parent.Id;
|
||||
timer.PrePaddingSeconds = seriesTimer.PrePaddingSeconds;
|
||||
timer.PostPaddingSeconds = seriesTimer.PostPaddingSeconds;
|
||||
timer.IsPostPaddingRequired = seriesTimer.IsPostPaddingRequired;
|
||||
timer.IsPrePaddingRequired = seriesTimer.IsPrePaddingRequired;
|
||||
timer.KeepUntil = seriesTimer.KeepUntil;
|
||||
timer.Priority = seriesTimer.Priority;
|
||||
timer.Name = parent.Name;
|
||||
timer.Overview = parent.Overview;
|
||||
timer.SeriesTimerId = seriesTimer.Id;
|
||||
|
||||
CopyProgramInfoToTimerInfo(parent, timer);
|
||||
|
||||
return timer;
|
||||
}
|
||||
|
||||
public static void CopyProgramInfoToTimerInfo(ProgramInfo programInfo, TimerInfo timerInfo)
|
||||
{
|
||||
timerInfo.SeasonNumber = programInfo.SeasonNumber;
|
||||
timerInfo.EpisodeNumber = programInfo.EpisodeNumber;
|
||||
timerInfo.IsMovie = programInfo.IsMovie;
|
||||
timerInfo.IsKids = programInfo.IsKids;
|
||||
timerInfo.IsNews = programInfo.IsNews;
|
||||
timerInfo.IsSports = programInfo.IsSports;
|
||||
timerInfo.ProductionYear = programInfo.ProductionYear;
|
||||
timerInfo.EpisodeTitle = programInfo.EpisodeTitle;
|
||||
timerInfo.OriginalAirDate = programInfo.OriginalAirDate;
|
||||
timerInfo.IsProgramSeries = programInfo.IsSeries;
|
||||
|
||||
timerInfo.HomePageUrl = programInfo.HomePageUrl;
|
||||
timerInfo.CommunityRating = programInfo.CommunityRating;
|
||||
timerInfo.ShortOverview = programInfo.ShortOverview;
|
||||
timerInfo.OfficialRating = programInfo.OfficialRating;
|
||||
timerInfo.IsRepeat = programInfo.IsRepeat;
|
||||
}
|
||||
|
||||
public static string GetRecordingName(TimerInfo info)
|
||||
{
|
||||
var name = info.Name;
|
||||
|
||||
if (info.IsProgramSeries)
|
||||
{
|
||||
var addHyphen = true;
|
||||
|
||||
if (info.SeasonNumber.HasValue && info.EpisodeNumber.HasValue)
|
||||
{
|
||||
name += string.Format(" S{0}E{1}", info.SeasonNumber.Value.ToString("00", CultureInfo.InvariantCulture), info.EpisodeNumber.Value.ToString("00", CultureInfo.InvariantCulture));
|
||||
addHyphen = false;
|
||||
}
|
||||
else if (info.OriginalAirDate.HasValue)
|
||||
{
|
||||
name += " " + info.OriginalAirDate.Value.ToString("yyyy-MM-dd");
|
||||
}
|
||||
else
|
||||
{
|
||||
name += " " + DateTime.Now.ToString("yyyy-MM-dd");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(info.EpisodeTitle))
|
||||
{
|
||||
if (addHyphen)
|
||||
{
|
||||
name += " -";
|
||||
}
|
||||
|
||||
name += " " + info.EpisodeTitle;
|
||||
}
|
||||
}
|
||||
|
||||
else if (info.IsMovie && info.ProductionYear != null)
|
||||
{
|
||||
name += " (" + info.ProductionYear + ")";
|
||||
}
|
||||
else
|
||||
{
|
||||
name += " " + info.StartDate.ToString("yyyy-MM-dd") + " " + info.Id;
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using System;
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Model.IO;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
|
||||
{
|
||||
public class SeriesTimerManager : ItemDataProvider<SeriesTimerInfo>
|
||||
{
|
||||
public SeriesTimerManager(IFileSystem fileSystem, IJsonSerializer jsonSerializer, ILogger logger, string dataPath)
|
||||
: base(fileSystem, jsonSerializer, logger, dataPath, (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
}
|
||||
|
||||
public override void Add(SeriesTimerInfo item)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(item.Id))
|
||||
{
|
||||
throw new ArgumentException("SeriesTimerInfo.Id cannot be null or empty.");
|
||||
}
|
||||
|
||||
base.Add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
using MediaBrowser.Common.Events;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Model.Events;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.LiveTv;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
|
||||
{
|
||||
public class TimerManager : ItemDataProvider<TimerInfo>
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, Timer> _timers = new ConcurrentDictionary<string, Timer>(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public event EventHandler<GenericEventArgs<TimerInfo>> TimerFired;
|
||||
|
||||
public TimerManager(IFileSystem fileSystem, IJsonSerializer jsonSerializer, ILogger logger, string dataPath, ILogger logger1)
|
||||
: base(fileSystem, jsonSerializer, logger, dataPath, (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger = logger1;
|
||||
}
|
||||
|
||||
public void RestartTimers()
|
||||
{
|
||||
StopTimers();
|
||||
|
||||
foreach (var item in GetAll().ToList())
|
||||
{
|
||||
AddOrUpdateSystemTimer(item);
|
||||
}
|
||||
}
|
||||
|
||||
public void StopTimers()
|
||||
{
|
||||
foreach (var pair in _timers.ToList())
|
||||
{
|
||||
pair.Value.Dispose();
|
||||
}
|
||||
|
||||
_timers.Clear();
|
||||
}
|
||||
|
||||
public override void Delete(TimerInfo item)
|
||||
{
|
||||
base.Delete(item);
|
||||
StopTimer(item);
|
||||
}
|
||||
|
||||
public override void Update(TimerInfo item)
|
||||
{
|
||||
base.Update(item);
|
||||
AddOrUpdateSystemTimer(item);
|
||||
}
|
||||
|
||||
public void AddOrUpdate(TimerInfo item, bool resetTimer)
|
||||
{
|
||||
if (resetTimer)
|
||||
{
|
||||
AddOrUpdate(item);
|
||||
return;
|
||||
}
|
||||
|
||||
var list = GetAll().ToList();
|
||||
|
||||
if (!list.Any(i => EqualityComparer(i, item)))
|
||||
{
|
||||
base.Add(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
base.Update(item);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Add(TimerInfo item)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(item.Id))
|
||||
{
|
||||
throw new ArgumentException("TimerInfo.Id cannot be null or empty.");
|
||||
}
|
||||
|
||||
base.Add(item);
|
||||
AddOrUpdateSystemTimer(item);
|
||||
}
|
||||
|
||||
private bool ShouldStartTimer(TimerInfo item)
|
||||
{
|
||||
if (item.Status == RecordingStatus.Completed ||
|
||||
item.Status == RecordingStatus.Cancelled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void AddOrUpdateSystemTimer(TimerInfo item)
|
||||
{
|
||||
StopTimer(item);
|
||||
|
||||
if (!ShouldStartTimer(item))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var startDate = RecordingHelper.GetStartTime(item);
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
if (startDate < now)
|
||||
{
|
||||
EventHelper.FireEventIfNotNull(TimerFired, this, new GenericEventArgs<TimerInfo> { Argument = item }, Logger);
|
||||
return;
|
||||
}
|
||||
|
||||
var dueTime = startDate - now;
|
||||
StartTimer(item, dueTime);
|
||||
}
|
||||
|
||||
private void StartTimer(TimerInfo item, TimeSpan dueTime)
|
||||
{
|
||||
var timer = new Timer(TimerCallback, item.Id, dueTime, TimeSpan.Zero);
|
||||
|
||||
if (_timers.TryAdd(item.Id, timer))
|
||||
{
|
||||
_logger.Info("Creating recording timer for {0}, {1}. Timer will fire in {2} minutes", item.Id, item.Name, dueTime.TotalMinutes.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
else
|
||||
{
|
||||
timer.Dispose();
|
||||
_logger.Warn("Timer already exists for item {0}", item.Id);
|
||||
}
|
||||
}
|
||||
|
||||
private void StopTimer(TimerInfo item)
|
||||
{
|
||||
Timer timer;
|
||||
if (_timers.TryRemove(item.Id, out timer))
|
||||
{
|
||||
timer.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private void TimerCallback(object state)
|
||||
{
|
||||
var timerId = (string)state;
|
||||
|
||||
var timer = GetAll().FirstOrDefault(i => string.Equals(i.Id, timerId, StringComparison.OrdinalIgnoreCase));
|
||||
if (timer != null)
|
||||
{
|
||||
EventHelper.FireEventIfNotNull(TimerFired, this, new GenericEventArgs<TimerInfo> { Argument = timer }, Logger);
|
||||
}
|
||||
}
|
||||
|
||||
public TimerInfo GetTimer(string id)
|
||||
{
|
||||
return GetAll().FirstOrDefault(r => string.Equals(r.Id, id, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,233 +0,0 @@
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.LiveTv;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.XmlTv.Classes;
|
||||
using Emby.XmlTv.Entities;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Model.Logging;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.LiveTv.Listings
|
||||
{
|
||||
public class XmlTvListingsProvider : IListingsProvider
|
||||
{
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public XmlTvListingsProvider(IServerConfigurationManager config, IHttpClient httpClient, ILogger logger)
|
||||
{
|
||||
_config = config;
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public string Name
|
||||
{
|
||||
get { return "XmlTV"; }
|
||||
}
|
||||
|
||||
public string Type
|
||||
{
|
||||
get { return "xmltv"; }
|
||||
}
|
||||
|
||||
private string GetLanguage()
|
||||
{
|
||||
return _config.Configuration.PreferredMetadataLanguage;
|
||||
}
|
||||
|
||||
private async Task<string> GetXml(string path, CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.Info("xmltv path: {0}", path);
|
||||
|
||||
if (!path.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
var cacheFilename = DateTime.UtcNow.DayOfYear.ToString(CultureInfo.InvariantCulture) + "-" + DateTime.UtcNow.Hour.ToString(CultureInfo.InvariantCulture) + ".xml";
|
||||
var cacheFile = Path.Combine(_config.ApplicationPaths.CachePath, "xmltv", cacheFilename);
|
||||
if (File.Exists(cacheFile))
|
||||
{
|
||||
return cacheFile;
|
||||
}
|
||||
|
||||
_logger.Info("Downloading xmltv listings from {0}", path);
|
||||
|
||||
var tempFile = await _httpClient.GetTempFile(new HttpRequestOptions
|
||||
{
|
||||
CancellationToken = cancellationToken,
|
||||
Url = path,
|
||||
Progress = new Progress<Double>(),
|
||||
DecompressionMethod = CompressionMethod.Gzip,
|
||||
|
||||
// It's going to come back gzipped regardless of this value
|
||||
// So we need to make sure the decompression method is set to gzip
|
||||
EnableHttpCompression = true
|
||||
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(cacheFile));
|
||||
|
||||
using (var stream = File.OpenRead(tempFile))
|
||||
{
|
||||
using (var reader = new StreamReader(stream, Encoding.UTF8))
|
||||
{
|
||||
using (var fileStream = File.OpenWrite(cacheFile))
|
||||
{
|
||||
using (var writer = new StreamWriter(fileStream))
|
||||
{
|
||||
while (!reader.EndOfStream)
|
||||
{
|
||||
writer.WriteLine(reader.ReadLine());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_logger.Debug("Returning xmltv path {0}", cacheFile);
|
||||
return cacheFile;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ProgramInfo>> GetProgramsAsync(ListingsProviderInfo info, string channelNumber, string channelName, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!await EmbyTV.EmbyTVRegistration.Instance.EnableXmlTv().ConfigureAwait(false))
|
||||
{
|
||||
var length = endDateUtc - startDateUtc;
|
||||
if (length.TotalDays > 1)
|
||||
{
|
||||
endDateUtc = startDateUtc.AddDays(1);
|
||||
}
|
||||
}
|
||||
|
||||
var path = await GetXml(info.Path, cancellationToken).ConfigureAwait(false);
|
||||
var reader = new XmlTvReader(path, GetLanguage(), null);
|
||||
|
||||
var results = reader.GetProgrammes(channelNumber, startDateUtc, endDateUtc, cancellationToken);
|
||||
return results.Select(p => GetProgramInfo(p, info));
|
||||
}
|
||||
|
||||
private ProgramInfo GetProgramInfo(XmlTvProgram p, ListingsProviderInfo info)
|
||||
{
|
||||
var programInfo = new ProgramInfo
|
||||
{
|
||||
ChannelId = p.ChannelId,
|
||||
EndDate = GetDate(p.EndDate),
|
||||
EpisodeNumber = p.Episode == null ? null : p.Episode.Episode,
|
||||
EpisodeTitle = p.Episode == null ? null : p.Episode.Title,
|
||||
Genres = p.Categories,
|
||||
Id = String.Format("{0}_{1:O}", p.ChannelId, p.StartDate), // Construct an id from the channel and start date,
|
||||
StartDate = GetDate(p.StartDate),
|
||||
Name = p.Title,
|
||||
Overview = p.Description,
|
||||
ShortOverview = p.Description,
|
||||
ProductionYear = !p.CopyrightDate.HasValue ? (int?)null : p.CopyrightDate.Value.Year,
|
||||
SeasonNumber = p.Episode == null ? null : p.Episode.Series,
|
||||
IsSeries = p.Episode != null,
|
||||
IsRepeat = p.IsRepeat,
|
||||
IsPremiere = p.Premiere != null,
|
||||
IsKids = p.Categories.Any(c => info.KidsCategories.Contains(c, StringComparer.InvariantCultureIgnoreCase)),
|
||||
IsMovie = p.Categories.Any(c => info.MovieCategories.Contains(c, StringComparer.InvariantCultureIgnoreCase)),
|
||||
IsNews = p.Categories.Any(c => info.NewsCategories.Contains(c, StringComparer.InvariantCultureIgnoreCase)),
|
||||
IsSports = p.Categories.Any(c => info.SportsCategories.Contains(c, StringComparer.InvariantCultureIgnoreCase)),
|
||||
ImageUrl = p.Icon != null && !String.IsNullOrEmpty(p.Icon.Source) ? p.Icon.Source : null,
|
||||
HasImage = p.Icon != null && !String.IsNullOrEmpty(p.Icon.Source),
|
||||
OfficialRating = p.Rating != null && !String.IsNullOrEmpty(p.Rating.Value) ? p.Rating.Value : null,
|
||||
CommunityRating = p.StarRating.HasValue ? p.StarRating.Value : (float?)null,
|
||||
SeriesId = p.Episode != null ? p.Title.GetMD5().ToString("N") : null
|
||||
};
|
||||
|
||||
if (programInfo.IsMovie)
|
||||
{
|
||||
programInfo.IsSeries = false;
|
||||
programInfo.EpisodeNumber = null;
|
||||
programInfo.EpisodeTitle = null;
|
||||
}
|
||||
|
||||
return programInfo;
|
||||
}
|
||||
|
||||
private DateTime GetDate(DateTime date)
|
||||
{
|
||||
if (date.Kind != DateTimeKind.Utc)
|
||||
{
|
||||
date = DateTime.SpecifyKind(date, DateTimeKind.Utc);
|
||||
}
|
||||
return date;
|
||||
}
|
||||
|
||||
public async Task AddMetadata(ListingsProviderInfo info, List<ChannelInfo> channels, CancellationToken cancellationToken)
|
||||
{
|
||||
// Add the channel image url
|
||||
var path = await GetXml(info.Path, cancellationToken).ConfigureAwait(false);
|
||||
var reader = new XmlTvReader(path, GetLanguage(), null);
|
||||
var results = reader.GetChannels().ToList();
|
||||
|
||||
if (channels != null)
|
||||
{
|
||||
channels.ForEach(c =>
|
||||
{
|
||||
var channelNumber = info.GetMappedChannel(c.Number);
|
||||
var match = results.FirstOrDefault(r => string.Equals(r.Id, channelNumber, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (match != null && match.Icon != null && !String.IsNullOrEmpty(match.Icon.Source))
|
||||
{
|
||||
c.ImageUrl = match.Icon.Source;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public Task Validate(ListingsProviderInfo info, bool validateLogin, bool validateListings)
|
||||
{
|
||||
// Assume all urls are valid. check files for existence
|
||||
if (!info.Path.StartsWith("http", StringComparison.OrdinalIgnoreCase) && !File.Exists(info.Path))
|
||||
{
|
||||
throw new FileNotFoundException("Could not find the XmlTv file specified:", info.Path);
|
||||
}
|
||||
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
public async Task<List<NameIdPair>> GetLineups(ListingsProviderInfo info, string country, string location)
|
||||
{
|
||||
// In theory this should never be called because there is always only one lineup
|
||||
var path = await GetXml(info.Path, CancellationToken.None).ConfigureAwait(false);
|
||||
var reader = new XmlTvReader(path, GetLanguage(), null);
|
||||
var results = reader.GetChannels();
|
||||
|
||||
// Should this method be async?
|
||||
return results.Select(c => new NameIdPair() { Id = c.Id, Name = c.DisplayName }).ToList();
|
||||
}
|
||||
|
||||
public async Task<List<ChannelInfo>> GetChannels(ListingsProviderInfo info, CancellationToken cancellationToken)
|
||||
{
|
||||
// In theory this should never be called because there is always only one lineup
|
||||
var path = await GetXml(info.Path, cancellationToken).ConfigureAwait(false);
|
||||
var reader = new XmlTvReader(path, GetLanguage(), null);
|
||||
var results = reader.GetChannels();
|
||||
|
||||
// Should this method be async?
|
||||
return results.Select(c => new ChannelInfo()
|
||||
{
|
||||
Id = c.Id,
|
||||
Name = c.DisplayName,
|
||||
ImageUrl = c.Icon != null && !String.IsNullOrEmpty(c.Icon.Source) ? c.Icon.Source : null,
|
||||
Number = c.Id
|
||||
|
||||
}).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Logging;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
{
|
||||
public class LiveStreamHelper
|
||||
{
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public LiveStreamHelper(IMediaEncoder mediaEncoder, ILogger logger)
|
||||
{
|
||||
_mediaEncoder = mediaEncoder;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task AddMediaInfoWithProbe(MediaSourceInfo mediaSource, bool isAudio, CancellationToken cancellationToken)
|
||||
{
|
||||
var originalRuntime = mediaSource.RunTimeTicks;
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
var info = await _mediaEncoder.GetMediaInfo(new MediaInfoRequest
|
||||
{
|
||||
InputPath = mediaSource.Path,
|
||||
Protocol = mediaSource.Protocol,
|
||||
MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video,
|
||||
ExtractChapters = false,
|
||||
AnalyzeDurationSections = 2
|
||||
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
_logger.Info("Live tv media info probe took {0} seconds", (DateTime.UtcNow - now).TotalSeconds.ToString(CultureInfo.InvariantCulture));
|
||||
|
||||
mediaSource.Bitrate = info.Bitrate;
|
||||
mediaSource.Container = info.Container;
|
||||
mediaSource.Formats = info.Formats;
|
||||
mediaSource.MediaStreams = info.MediaStreams;
|
||||
mediaSource.RunTimeTicks = info.RunTimeTicks;
|
||||
mediaSource.Size = info.Size;
|
||||
mediaSource.Timestamp = info.Timestamp;
|
||||
mediaSource.Video3DFormat = info.Video3DFormat;
|
||||
mediaSource.VideoType = info.VideoType;
|
||||
|
||||
mediaSource.DefaultSubtitleStreamIndex = null;
|
||||
|
||||
// Null this out so that it will be treated like a live stream
|
||||
if (!originalRuntime.HasValue)
|
||||
{
|
||||
mediaSource.RunTimeTicks = null;
|
||||
}
|
||||
|
||||
var audioStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == Model.Entities.MediaStreamType.Audio);
|
||||
|
||||
if (audioStream == null || audioStream.Index == -1)
|
||||
{
|
||||
mediaSource.DefaultAudioStreamIndex = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
mediaSource.DefaultAudioStreamIndex = audioStream.Index;
|
||||
}
|
||||
|
||||
var videoStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == Model.Entities.MediaStreamType.Video);
|
||||
if (videoStream != null)
|
||||
{
|
||||
if (!videoStream.BitRate.HasValue)
|
||||
{
|
||||
var width = videoStream.Width ?? 1920;
|
||||
|
||||
if (width >= 1900)
|
||||
{
|
||||
videoStream.BitRate = 8000000;
|
||||
}
|
||||
|
||||
else if (width >= 1260)
|
||||
{
|
||||
videoStream.BitRate = 3000000;
|
||||
}
|
||||
|
||||
else if (width >= 700)
|
||||
{
|
||||
videoStream.BitRate = 1000000;
|
||||
}
|
||||
}
|
||||
|
||||
// This is coming up false and preventing stream copy
|
||||
videoStream.IsAVC = null;
|
||||
}
|
||||
|
||||
// Try to estimate this
|
||||
if (!mediaSource.Bitrate.HasValue)
|
||||
{
|
||||
var total = mediaSource.MediaStreams.Select(i => i.BitRate ?? 0).Sum();
|
||||
|
||||
if (total > 0)
|
||||
{
|
||||
mediaSource.Bitrate = total;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Model.LiveTv;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
{
|
||||
public class LiveTvConfigurationFactory : IConfigurationFactory
|
||||
{
|
||||
public IEnumerable<ConfigurationStore> GetConfigurations()
|
||||
{
|
||||
return new List<ConfigurationStore>
|
||||
{
|
||||
new ConfigurationStore
|
||||
{
|
||||
ConfigurationType = typeof(LiveTvOptions),
|
||||
Key = "livetv"
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,390 +0,0 @@
|
||||
using MediaBrowser.Common;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.LiveTv;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
{
|
||||
public class LiveTvDtoService
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly IImageProcessor _imageProcessor;
|
||||
|
||||
private readonly IUserDataManager _userDataManager;
|
||||
private readonly IDtoService _dtoService;
|
||||
private readonly IApplicationHost _appHost;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
|
||||
public LiveTvDtoService(IDtoService dtoService, IUserDataManager userDataManager, IImageProcessor imageProcessor, ILogger logger, IApplicationHost appHost, ILibraryManager libraryManager)
|
||||
{
|
||||
_dtoService = dtoService;
|
||||
_userDataManager = userDataManager;
|
||||
_imageProcessor = imageProcessor;
|
||||
_logger = logger;
|
||||
_appHost = appHost;
|
||||
_libraryManager = libraryManager;
|
||||
}
|
||||
|
||||
public TimerInfoDto GetTimerInfoDto(TimerInfo info, ILiveTvService service, LiveTvProgram program, LiveTvChannel channel)
|
||||
{
|
||||
var dto = new TimerInfoDto
|
||||
{
|
||||
Id = GetInternalTimerId(service.Name, info.Id).ToString("N"),
|
||||
Overview = info.Overview,
|
||||
EndDate = info.EndDate,
|
||||
Name = info.Name,
|
||||
StartDate = info.StartDate,
|
||||
ExternalId = info.Id,
|
||||
ChannelId = GetInternalChannelId(service.Name, info.ChannelId).ToString("N"),
|
||||
Status = info.Status,
|
||||
SeriesTimerId = string.IsNullOrEmpty(info.SeriesTimerId) ? null : GetInternalSeriesTimerId(service.Name, info.SeriesTimerId).ToString("N"),
|
||||
PrePaddingSeconds = info.PrePaddingSeconds,
|
||||
PostPaddingSeconds = info.PostPaddingSeconds,
|
||||
IsPostPaddingRequired = info.IsPostPaddingRequired,
|
||||
IsPrePaddingRequired = info.IsPrePaddingRequired,
|
||||
KeepUntil = info.KeepUntil,
|
||||
ExternalChannelId = info.ChannelId,
|
||||
ExternalSeriesTimerId = info.SeriesTimerId,
|
||||
ServiceName = service.Name,
|
||||
ExternalProgramId = info.ProgramId,
|
||||
Priority = info.Priority,
|
||||
RunTimeTicks = (info.EndDate - info.StartDate).Ticks,
|
||||
ServerId = _appHost.SystemId
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(info.ProgramId))
|
||||
{
|
||||
dto.ProgramId = GetInternalProgramId(service.Name, info.ProgramId).ToString("N");
|
||||
}
|
||||
|
||||
if (program != null)
|
||||
{
|
||||
dto.ProgramInfo = _dtoService.GetBaseItemDto(program, new DtoOptions());
|
||||
|
||||
if (info.Status != RecordingStatus.Cancelled && info.Status != RecordingStatus.Error)
|
||||
{
|
||||
dto.ProgramInfo.TimerId = dto.Id;
|
||||
dto.ProgramInfo.Status = info.Status.ToString();
|
||||
}
|
||||
|
||||
dto.ProgramInfo.SeriesTimerId = dto.SeriesTimerId;
|
||||
}
|
||||
|
||||
if (channel != null)
|
||||
{
|
||||
dto.ChannelName = channel.Name;
|
||||
}
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
public SeriesTimerInfoDto GetSeriesTimerInfoDto(SeriesTimerInfo info, ILiveTvService service, string channelName)
|
||||
{
|
||||
var dto = new SeriesTimerInfoDto
|
||||
{
|
||||
Id = GetInternalSeriesTimerId(service.Name, info.Id).ToString("N"),
|
||||
Overview = info.Overview,
|
||||
EndDate = info.EndDate,
|
||||
Name = info.Name,
|
||||
StartDate = info.StartDate,
|
||||
ExternalId = info.Id,
|
||||
PrePaddingSeconds = info.PrePaddingSeconds,
|
||||
PostPaddingSeconds = info.PostPaddingSeconds,
|
||||
IsPostPaddingRequired = info.IsPostPaddingRequired,
|
||||
IsPrePaddingRequired = info.IsPrePaddingRequired,
|
||||
Days = info.Days,
|
||||
Priority = info.Priority,
|
||||
RecordAnyChannel = info.RecordAnyChannel,
|
||||
RecordAnyTime = info.RecordAnyTime,
|
||||
SkipEpisodesInLibrary = info.SkipEpisodesInLibrary,
|
||||
KeepUpTo = info.KeepUpTo,
|
||||
KeepUntil = info.KeepUntil,
|
||||
RecordNewOnly = info.RecordNewOnly,
|
||||
ExternalChannelId = info.ChannelId,
|
||||
ExternalProgramId = info.ProgramId,
|
||||
ServiceName = service.Name,
|
||||
ChannelName = channelName,
|
||||
ServerId = _appHost.SystemId
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(info.ChannelId))
|
||||
{
|
||||
dto.ChannelId = GetInternalChannelId(service.Name, info.ChannelId).ToString("N");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(info.ProgramId))
|
||||
{
|
||||
dto.ProgramId = GetInternalProgramId(service.Name, info.ProgramId).ToString("N");
|
||||
}
|
||||
|
||||
dto.DayPattern = info.Days == null ? null : GetDayPattern(info.Days);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(info.SeriesId))
|
||||
{
|
||||
var program = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name },
|
||||
ExternalSeriesId = info.SeriesId,
|
||||
Limit = 1,
|
||||
ImageTypes = new ImageType[] { ImageType.Primary }
|
||||
|
||||
}).FirstOrDefault();
|
||||
|
||||
if (program != null)
|
||||
{
|
||||
var image = program.GetImageInfo(ImageType.Primary, 0);
|
||||
if (image != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
dto.ParentPrimaryImageTag = _imageProcessor.GetImageCacheTag(program, image);
|
||||
dto.ParentPrimaryImageItemId = program.Id.ToString("N");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
public DayPattern? GetDayPattern(List<DayOfWeek> days)
|
||||
{
|
||||
DayPattern? pattern = null;
|
||||
|
||||
if (days.Count > 0)
|
||||
{
|
||||
if (days.Count == 7)
|
||||
{
|
||||
pattern = DayPattern.Daily;
|
||||
}
|
||||
else if (days.Count == 2)
|
||||
{
|
||||
if (days.Contains(DayOfWeek.Saturday) && days.Contains(DayOfWeek.Sunday))
|
||||
{
|
||||
pattern = DayPattern.Weekends;
|
||||
}
|
||||
}
|
||||
else if (days.Count == 5)
|
||||
{
|
||||
if (days.Contains(DayOfWeek.Monday) && days.Contains(DayOfWeek.Tuesday) && days.Contains(DayOfWeek.Wednesday) && days.Contains(DayOfWeek.Thursday) && days.Contains(DayOfWeek.Friday))
|
||||
{
|
||||
pattern = DayPattern.Weekdays;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return pattern;
|
||||
}
|
||||
|
||||
public LiveTvTunerInfoDto GetTunerInfoDto(string serviceName, LiveTvTunerInfo info, string channelName)
|
||||
{
|
||||
var dto = new LiveTvTunerInfoDto
|
||||
{
|
||||
Name = info.Name,
|
||||
Id = info.Id,
|
||||
Clients = info.Clients,
|
||||
ProgramName = info.ProgramName,
|
||||
SourceType = info.SourceType,
|
||||
Status = info.Status,
|
||||
ChannelName = channelName,
|
||||
Url = info.Url,
|
||||
CanReset = info.CanReset
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(info.ChannelId))
|
||||
{
|
||||
dto.ChannelId = GetInternalChannelId(serviceName, info.ChannelId).ToString("N");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(info.RecordingId))
|
||||
{
|
||||
dto.RecordingId = GetInternalRecordingId(serviceName, info.RecordingId).ToString("N");
|
||||
}
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
internal string GetImageTag(IHasImages info)
|
||||
{
|
||||
try
|
||||
{
|
||||
return _imageProcessor.GetImageCacheTag(info, ImageType.Primary);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error getting image info for {0}", ex, info.Name);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private const string InternalVersionNumber = "4";
|
||||
|
||||
public Guid GetInternalChannelId(string serviceName, string externalId)
|
||||
{
|
||||
var name = serviceName + externalId + InternalVersionNumber;
|
||||
|
||||
return _libraryManager.GetNewItemId(name.ToLower(), typeof(LiveTvChannel));
|
||||
}
|
||||
|
||||
public Guid GetInternalTimerId(string serviceName, string externalId)
|
||||
{
|
||||
var name = serviceName + externalId + InternalVersionNumber;
|
||||
|
||||
return name.ToLower().GetMD5();
|
||||
}
|
||||
|
||||
public Guid GetInternalSeriesTimerId(string serviceName, string externalId)
|
||||
{
|
||||
var name = serviceName + externalId + InternalVersionNumber;
|
||||
|
||||
return name.ToLower().GetMD5();
|
||||
}
|
||||
|
||||
public Guid GetInternalProgramId(string serviceName, string externalId)
|
||||
{
|
||||
var name = serviceName + externalId + InternalVersionNumber;
|
||||
|
||||
return _libraryManager.GetNewItemId(name.ToLower(), typeof(LiveTvProgram));
|
||||
}
|
||||
|
||||
public Guid GetInternalRecordingId(string serviceName, string externalId)
|
||||
{
|
||||
var name = serviceName + externalId + InternalVersionNumber + "0";
|
||||
|
||||
return _libraryManager.GetNewItemId(name.ToLower(), typeof(ILiveTvRecording));
|
||||
}
|
||||
|
||||
public async Task<TimerInfo> GetTimerInfo(TimerInfoDto dto, bool isNew, LiveTvManager liveTv, CancellationToken cancellationToken)
|
||||
{
|
||||
var info = new TimerInfo
|
||||
{
|
||||
Overview = dto.Overview,
|
||||
EndDate = dto.EndDate,
|
||||
Name = dto.Name,
|
||||
StartDate = dto.StartDate,
|
||||
Status = dto.Status,
|
||||
PrePaddingSeconds = dto.PrePaddingSeconds,
|
||||
PostPaddingSeconds = dto.PostPaddingSeconds,
|
||||
IsPostPaddingRequired = dto.IsPostPaddingRequired,
|
||||
IsPrePaddingRequired = dto.IsPrePaddingRequired,
|
||||
KeepUntil = dto.KeepUntil,
|
||||
Priority = dto.Priority,
|
||||
SeriesTimerId = dto.ExternalSeriesTimerId,
|
||||
ProgramId = dto.ExternalProgramId,
|
||||
ChannelId = dto.ExternalChannelId,
|
||||
Id = dto.ExternalId
|
||||
};
|
||||
|
||||
// Convert internal server id's to external tv provider id's
|
||||
if (!isNew && !string.IsNullOrEmpty(dto.Id) && string.IsNullOrEmpty(info.Id))
|
||||
{
|
||||
var timer = await liveTv.GetSeriesTimer(dto.Id, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
info.Id = timer.ExternalId;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(dto.ChannelId) && string.IsNullOrEmpty(info.ChannelId))
|
||||
{
|
||||
var channel = liveTv.GetInternalChannel(dto.ChannelId);
|
||||
|
||||
if (channel != null)
|
||||
{
|
||||
info.ChannelId = channel.ExternalId;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(dto.ProgramId) && string.IsNullOrEmpty(info.ProgramId))
|
||||
{
|
||||
var program = liveTv.GetInternalProgram(dto.ProgramId);
|
||||
|
||||
if (program != null)
|
||||
{
|
||||
info.ProgramId = program.ExternalId;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(dto.SeriesTimerId) && string.IsNullOrEmpty(info.SeriesTimerId))
|
||||
{
|
||||
var timer = await liveTv.GetSeriesTimer(dto.SeriesTimerId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (timer != null)
|
||||
{
|
||||
info.SeriesTimerId = timer.ExternalId;
|
||||
}
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
public async Task<SeriesTimerInfo> GetSeriesTimerInfo(SeriesTimerInfoDto dto, bool isNew, LiveTvManager liveTv, CancellationToken cancellationToken)
|
||||
{
|
||||
var info = new SeriesTimerInfo
|
||||
{
|
||||
Overview = dto.Overview,
|
||||
EndDate = dto.EndDate,
|
||||
Name = dto.Name,
|
||||
StartDate = dto.StartDate,
|
||||
PrePaddingSeconds = dto.PrePaddingSeconds,
|
||||
PostPaddingSeconds = dto.PostPaddingSeconds,
|
||||
IsPostPaddingRequired = dto.IsPostPaddingRequired,
|
||||
IsPrePaddingRequired = dto.IsPrePaddingRequired,
|
||||
Days = dto.Days,
|
||||
Priority = dto.Priority,
|
||||
RecordAnyChannel = dto.RecordAnyChannel,
|
||||
RecordAnyTime = dto.RecordAnyTime,
|
||||
SkipEpisodesInLibrary = dto.SkipEpisodesInLibrary,
|
||||
KeepUpTo = dto.KeepUpTo,
|
||||
KeepUntil = dto.KeepUntil,
|
||||
RecordNewOnly = dto.RecordNewOnly,
|
||||
ProgramId = dto.ExternalProgramId,
|
||||
ChannelId = dto.ExternalChannelId,
|
||||
Id = dto.ExternalId
|
||||
};
|
||||
|
||||
// Convert internal server id's to external tv provider id's
|
||||
if (!isNew && !string.IsNullOrEmpty(dto.Id) && string.IsNullOrEmpty(info.Id))
|
||||
{
|
||||
var timer = await liveTv.GetSeriesTimer(dto.Id, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
info.Id = timer.ExternalId;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(dto.ChannelId) && string.IsNullOrEmpty(info.ChannelId))
|
||||
{
|
||||
var channel = liveTv.GetInternalChannel(dto.ChannelId);
|
||||
|
||||
if (channel != null)
|
||||
{
|
||||
info.ChannelId = channel.ExternalId;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(dto.ProgramId) && string.IsNullOrEmpty(info.ProgramId))
|
||||
{
|
||||
var program = liveTv.GetInternalProgram(dto.ProgramId);
|
||||
|
||||
if (program != null)
|
||||
{
|
||||
info.ProgramId = program.ExternalId;
|
||||
}
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,219 +0,0 @@
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
{
|
||||
public class LiveTvMediaSourceProvider : IMediaSourceProvider
|
||||
{
|
||||
private readonly ILiveTvManager _liveTvManager;
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
|
||||
public LiveTvMediaSourceProvider(ILiveTvManager liveTvManager, IJsonSerializer jsonSerializer, ILogManager logManager, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder, IServerApplicationHost appHost)
|
||||
{
|
||||
_liveTvManager = liveTvManager;
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
_appHost = appHost;
|
||||
_logger = logManager.GetLogger(GetType().Name);
|
||||
}
|
||||
|
||||
public Task<IEnumerable<MediaSourceInfo>> GetMediaSources(IHasMediaSources item, CancellationToken cancellationToken)
|
||||
{
|
||||
var baseItem = (BaseItem)item;
|
||||
|
||||
if (baseItem.SourceType == SourceType.LiveTV)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(baseItem.Path))
|
||||
{
|
||||
return GetMediaSourcesInternal(item, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
return Task.FromResult<IEnumerable<MediaSourceInfo>>(new List<MediaSourceInfo>());
|
||||
}
|
||||
|
||||
// Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message.
|
||||
private const char StreamIdDelimeter = '_';
|
||||
private const string StreamIdDelimeterString = "_";
|
||||
|
||||
private async Task<IEnumerable<MediaSourceInfo>> GetMediaSourcesInternal(IHasMediaSources item, CancellationToken cancellationToken)
|
||||
{
|
||||
IEnumerable<MediaSourceInfo> sources;
|
||||
|
||||
var forceRequireOpening = false;
|
||||
|
||||
try
|
||||
{
|
||||
if (item is ILiveTvRecording)
|
||||
{
|
||||
sources = await _liveTvManager.GetRecordingMediaSources(item, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
sources = await _liveTvManager.GetChannelMediaSources(item, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (NotImplementedException)
|
||||
{
|
||||
var hasMediaSources = (IHasMediaSources)item;
|
||||
|
||||
sources = _mediaSourceManager.GetStaticMediaSources(hasMediaSources, false)
|
||||
.ToList();
|
||||
|
||||
forceRequireOpening = true;
|
||||
}
|
||||
|
||||
var list = sources.ToList();
|
||||
var serverUrl = await _appHost.GetLocalApiUrl().ConfigureAwait(false);
|
||||
|
||||
foreach (var source in list)
|
||||
{
|
||||
source.Type = MediaSourceType.Default;
|
||||
source.BufferMs = source.BufferMs ?? 1500;
|
||||
|
||||
if (source.RequiresOpening || forceRequireOpening)
|
||||
{
|
||||
source.RequiresOpening = true;
|
||||
}
|
||||
|
||||
if (source.RequiresOpening)
|
||||
{
|
||||
var openKeys = new List<string>();
|
||||
openKeys.Add(item.GetType().Name);
|
||||
openKeys.Add(item.Id.ToString("N"));
|
||||
openKeys.Add(source.Id ?? string.Empty);
|
||||
source.OpenToken = string.Join(StreamIdDelimeterString, openKeys.ToArray());
|
||||
}
|
||||
|
||||
// Dummy this up so that direct play checks can still run
|
||||
if (string.IsNullOrEmpty(source.Path) && source.Protocol == MediaProtocol.Http)
|
||||
{
|
||||
source.Path = serverUrl;
|
||||
}
|
||||
}
|
||||
|
||||
_logger.Debug("MediaSources: {0}", _jsonSerializer.SerializeToString(list));
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
public async Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> OpenMediaSource(string openToken, CancellationToken cancellationToken)
|
||||
{
|
||||
MediaSourceInfo stream = null;
|
||||
const bool isAudio = false;
|
||||
|
||||
var keys = openToken.Split(new[] { StreamIdDelimeter }, 3);
|
||||
var mediaSourceId = keys.Length >= 3 ? keys[2] : null;
|
||||
IDirectStreamProvider directStreamProvider = null;
|
||||
|
||||
if (string.Equals(keys[0], typeof(LiveTvChannel).Name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var info = await _liveTvManager.GetChannelStream(keys[1], mediaSourceId, cancellationToken).ConfigureAwait(false);
|
||||
stream = info.Item1;
|
||||
directStreamProvider = info.Item2;
|
||||
}
|
||||
else
|
||||
{
|
||||
stream = await _liveTvManager.GetRecordingStream(keys[1], cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (!stream.SupportsProbing || stream.MediaStreams.Any(i => i.Index != -1))
|
||||
{
|
||||
await AddMediaInfo(stream, isAudio, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await new LiveStreamHelper(_mediaEncoder, _logger).AddMediaInfoWithProbe(stream, isAudio, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error probing live tv stream", ex);
|
||||
}
|
||||
|
||||
return new Tuple<MediaSourceInfo, IDirectStreamProvider>(stream, directStreamProvider);
|
||||
}
|
||||
|
||||
private async Task AddMediaInfo(MediaSourceInfo mediaSource, bool isAudio, CancellationToken cancellationToken)
|
||||
{
|
||||
mediaSource.DefaultSubtitleStreamIndex = null;
|
||||
|
||||
// Null this out so that it will be treated like a live stream
|
||||
mediaSource.RunTimeTicks = null;
|
||||
|
||||
var audioStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == Model.Entities.MediaStreamType.Audio);
|
||||
|
||||
if (audioStream == null || audioStream.Index == -1)
|
||||
{
|
||||
mediaSource.DefaultAudioStreamIndex = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
mediaSource.DefaultAudioStreamIndex = audioStream.Index;
|
||||
}
|
||||
|
||||
var videoStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == Model.Entities.MediaStreamType.Video);
|
||||
if (videoStream != null)
|
||||
{
|
||||
if (!videoStream.BitRate.HasValue)
|
||||
{
|
||||
var width = videoStream.Width ?? 1920;
|
||||
|
||||
if (width >= 1900)
|
||||
{
|
||||
videoStream.BitRate = 8000000;
|
||||
}
|
||||
|
||||
else if (width >= 1260)
|
||||
{
|
||||
videoStream.BitRate = 3000000;
|
||||
}
|
||||
|
||||
else if (width >= 700)
|
||||
{
|
||||
videoStream.BitRate = 1000000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try to estimate this
|
||||
if (!mediaSource.Bitrate.HasValue)
|
||||
{
|
||||
var total = mediaSource.MediaStreams.Select(i => i.BitRate ?? 0).Sum();
|
||||
|
||||
if (total > 0)
|
||||
{
|
||||
mediaSource.Bitrate = total;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Task CloseMediaSource(string liveStreamId)
|
||||
{
|
||||
return _liveTvManager.CloseLiveStream(liveStreamId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Model.LiveTv;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
{
|
||||
public class RefreshChannelsScheduledTask : IScheduledTask, IConfigurableScheduledTask
|
||||
{
|
||||
private readonly ILiveTvManager _liveTvManager;
|
||||
private readonly IConfigurationManager _config;
|
||||
|
||||
public RefreshChannelsScheduledTask(ILiveTvManager liveTvManager, IConfigurationManager config)
|
||||
{
|
||||
_liveTvManager = liveTvManager;
|
||||
_config = config;
|
||||
}
|
||||
|
||||
public string Name
|
||||
{
|
||||
get { return "Refresh Guide"; }
|
||||
}
|
||||
|
||||
public string Description
|
||||
{
|
||||
get { return "Downloads channel information from live tv services."; }
|
||||
}
|
||||
|
||||
public string Category
|
||||
{
|
||||
get { return "Live TV"; }
|
||||
}
|
||||
|
||||
public Task Execute(System.Threading.CancellationToken cancellationToken, IProgress<double> progress)
|
||||
{
|
||||
var manager = (LiveTvManager)_liveTvManager;
|
||||
|
||||
return manager.RefreshChannels(progress, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the triggers that define when the task will run
|
||||
/// </summary>
|
||||
/// <returns>IEnumerable{BaseTaskTrigger}.</returns>
|
||||
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
|
||||
{
|
||||
return new[] {
|
||||
|
||||
// Every so often
|
||||
new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(12).Ticks}
|
||||
};
|
||||
}
|
||||
|
||||
private LiveTvOptions GetConfiguration()
|
||||
{
|
||||
return _config.GetConfiguration<LiveTvOptions>("livetv");
|
||||
}
|
||||
|
||||
public bool IsHidden
|
||||
{
|
||||
get { return _liveTvManager.Services.Count == 1 && GetConfiguration().TunerHosts.Count(i => i.IsEnabled) == 0; }
|
||||
}
|
||||
|
||||
public bool IsEnabled
|
||||
{
|
||||
get { return true; }
|
||||
}
|
||||
|
||||
public bool IsLogged
|
||||
{
|
||||
get { return true; }
|
||||
}
|
||||
|
||||
public string Key
|
||||
{
|
||||
get { return "RefreshGuide"; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -144,32 +144,6 @@
|
||||
<Compile Include="IO\FileRefresher.cs" />
|
||||
<Compile Include="IO\LibraryMonitor.cs" />
|
||||
<Compile Include="IO\MemoryStreamProvider.cs" />
|
||||
<Compile Include="LiveTv\EmbyTV\DirectRecorder.cs" />
|
||||
<Compile Include="LiveTv\EmbyTV\EmbyTV.cs" />
|
||||
<Compile Include="LiveTv\EmbyTV\EmbyTVRegistration.cs" />
|
||||
<Compile Include="LiveTv\EmbyTV\EncodedRecorder.cs" />
|
||||
<Compile Include="LiveTv\EmbyTV\EntryPoint.cs" />
|
||||
<Compile Include="LiveTv\EmbyTV\IRecorder.cs" />
|
||||
<Compile Include="LiveTv\EmbyTV\ItemDataProvider.cs" />
|
||||
<Compile Include="LiveTv\EmbyTV\RecordingHelper.cs" />
|
||||
<Compile Include="LiveTv\EmbyTV\SeriesTimerManager.cs" />
|
||||
<Compile Include="LiveTv\EmbyTV\TimerManager.cs" />
|
||||
<Compile Include="LiveTv\Listings\SchedulesDirect.cs" />
|
||||
<Compile Include="LiveTv\Listings\XmlTvListingsProvider.cs" />
|
||||
<Compile Include="LiveTv\LiveStreamHelper.cs" />
|
||||
<Compile Include="LiveTv\LiveTvConfigurationFactory.cs" />
|
||||
<Compile Include="LiveTv\LiveTvDtoService.cs" />
|
||||
<Compile Include="LiveTv\LiveTvManager.cs" />
|
||||
<Compile Include="LiveTv\LiveTvMediaSourceProvider.cs" />
|
||||
<Compile Include="LiveTv\TunerHosts\BaseTunerHost.cs" />
|
||||
<Compile Include="LiveTv\TunerHosts\HdHomerun\HdHomerunHost.cs" />
|
||||
<Compile Include="LiveTv\TunerHosts\HdHomerun\HdHomerunDiscovery.cs" />
|
||||
<Compile Include="LiveTv\TunerHosts\HdHomerun\HdHomerunLiveStream.cs" />
|
||||
<Compile Include="LiveTv\TunerHosts\M3uParser.cs" />
|
||||
<Compile Include="LiveTv\TunerHosts\M3UTunerHost.cs" />
|
||||
<Compile Include="LiveTv\RefreshChannelsScheduledTask.cs" />
|
||||
<Compile Include="LiveTv\TunerHosts\MulticastStream.cs" />
|
||||
<Compile Include="LiveTv\TunerHosts\QueueStream.cs" />
|
||||
<Compile Include="LiveTv\TunerHosts\SatIp\ChannelScan.cs" />
|
||||
<Compile Include="LiveTv\TunerHosts\SatIp\Rtcp\ReportBlock.cs" />
|
||||
<Compile Include="LiveTv\TunerHosts\SatIp\Rtcp\RtcpAppPacket.cs" />
|
||||
@@ -188,8 +162,8 @@
|
||||
<Compile Include="LiveTv\TunerHosts\SatIp\Rtsp\RtspResponse.cs" />
|
||||
<Compile Include="LiveTv\TunerHosts\SatIp\Rtsp\RtspSession.cs" />
|
||||
<Compile Include="LiveTv\TunerHosts\SatIp\Rtsp\RtspStatusCode.cs" />
|
||||
<Compile Include="LiveTv\TunerHosts\SatIp\SatIpHost.cs" />
|
||||
<Compile Include="LiveTv\TunerHosts\SatIp\SatIpDiscovery.cs" />
|
||||
<Compile Include="LiveTv\TunerHosts\SatIp\SatIpHost.cs" />
|
||||
<Compile Include="LiveTv\TunerHosts\SatIp\TransmissionMode.cs" />
|
||||
<Compile Include="LiveTv\TunerHosts\SatIp\Utils.cs" />
|
||||
<Compile Include="Localization\LocalizationManager.cs" />
|
||||
@@ -212,12 +186,6 @@
|
||||
<Compile Include="Security\AuthenticationRepository.cs" />
|
||||
<Compile Include="Security\EncryptionManager.cs" />
|
||||
<Compile Include="ServerApplicationPaths.cs" />
|
||||
<Compile Include="Session\HttpSessionController.cs" />
|
||||
<Compile Include="Session\SessionManager.cs">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Include="Session\SessionWebSocketListener.cs" />
|
||||
<Compile Include="Session\WebSocketController.cs" />
|
||||
<Compile Include="Persistence\SqliteDisplayPreferencesRepository.cs" />
|
||||
<Compile Include="Persistence\SqliteItemRepository.cs" />
|
||||
<Compile Include="Persistence\SqliteUserDataRepository.cs" />
|
||||
@@ -227,6 +195,10 @@
|
||||
<Compile Include="Udp\UdpServer.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Emby.Server.Implementations\Emby.Server.Implementations.csproj">
|
||||
<Project>{e383961b-9356-4d5d-8233-9a1079d03055}</Project>
|
||||
<Name>Emby.Server.Implementations</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj">
|
||||
<Project>{9142EEFA-7570-41E1-BFCC-468BB571AF2F}</Project>
|
||||
<Name>MediaBrowser.Common</Name>
|
||||
@@ -360,175 +332,6 @@
|
||||
<EmbeddedResource Include="Localization\Ratings\au.txt" />
|
||||
<EmbeddedResource Include="Localization\iso6392.txt" />
|
||||
<None Include="app.config" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0030.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0049.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0070.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0090.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0100.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0130.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0160.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0170.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0192.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0200.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0215.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0235.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0255.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0260.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0282.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0305.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0308.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0310.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0315.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0330.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0360.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0380.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0390.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0400.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0420.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0435.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0450.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0460.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0475.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0480.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0490.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0505.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0510.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0520.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0525.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0530.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0549.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0560.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0570.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0600.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0620.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0642.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0650.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0660.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0685.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0705.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0721.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0740.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0750.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0765.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0785.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0830.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0851.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0865.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0875.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0880.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0900.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0915.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0922.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0935.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0950.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\0965.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\1005.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\1030.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\1055.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\1082.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\1100.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\1105.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\1130.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\1155.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\1160.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\1180.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\1195.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\1222.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\1240.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\1250.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\1280.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\1320.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\1340.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\1380.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\1400.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\1440.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\1500.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\1520.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\1540.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\1560.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\1590.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\1600 OPTUS D1 FTA %28160.0E%29.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\1600.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\1620.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\1640.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\1660.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\1690.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\1720.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\1800.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\1830.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\2210.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\2230.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\2250.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\2270.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\2290.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\2310.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\2330.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\2350.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\2370.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\2390.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\2410.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\2432.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\2451.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\2470.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\2489.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\2500.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\2527.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\2550.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\2570.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\2590.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\2608.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\2630.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\2650.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\2669.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\2690.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\2710.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\2728.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\2730.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\2750.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\2760.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\2770.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\2780.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\2812.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\2820.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\2830.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\2850.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\2873.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\2880.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\2881.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\2882.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\2900.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\2930.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\2950.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\2970.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\2985.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\2990.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\3020.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\3045.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\3070.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\3100.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\3125.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\3150.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\3169.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\3195.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\3225.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\3255.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\3285.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\3300.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\3325.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\3355.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\3380.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\3400.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\3420.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\3450.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\3460.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\3475.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\3490.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\3520.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\3527.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\3550.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\3560.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\3592.ini" />
|
||||
<EmbeddedResource Include="LiveTv\TunerHosts\SatIp\ini\satellite\3594.ini" />
|
||||
<EmbeddedResource Include="Localization\Core\ar.json" />
|
||||
<EmbeddedResource Include="Localization\Core\bg-BG.json" />
|
||||
<EmbeddedResource Include="Localization\Core\ca.json" />
|
||||
@@ -570,6 +373,175 @@
|
||||
<EmbeddedResource Include="Localization\Core\zh-HK.json" />
|
||||
<EmbeddedResource Include="Localization\Core\zh-TW.json" />
|
||||
<EmbeddedResource Include="Localization\countries.json" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0030.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0049.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0070.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0090.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0100.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0130.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0160.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0170.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0192.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0200.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0215.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0235.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0255.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0260.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0282.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0305.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0308.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0310.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0315.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0330.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0360.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0380.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0390.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0400.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0420.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0435.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0450.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0460.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0475.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0480.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0490.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0505.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0510.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0520.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0525.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0530.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0549.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0560.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0570.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0600.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0620.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0642.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0650.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0660.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0685.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0705.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0721.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0740.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0750.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0765.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0785.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0830.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0851.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0865.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0875.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0880.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0900.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0915.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0922.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0935.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0950.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\0965.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\1005.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\1030.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\1055.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\1082.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\1100.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\1105.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\1130.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\1155.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\1160.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\1180.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\1195.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\1222.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\1240.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\1250.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\1280.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\1320.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\1340.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\1380.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\1400.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\1440.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\1500.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\1520.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\1540.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\1560.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\1590.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\1600 OPTUS D1 FTA %28160.0E%29.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\1600.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\1620.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\1640.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\1660.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\1690.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\1720.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\1800.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\1830.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\2210.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\2230.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\2250.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\2270.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\2290.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\2310.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\2330.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\2350.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\2370.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\2390.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\2410.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\2432.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\2451.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\2470.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\2489.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\2500.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\2527.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\2550.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\2570.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\2590.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\2608.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\2630.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\2650.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\2669.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\2690.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\2710.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\2728.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\2730.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\2750.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\2760.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\2770.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\2780.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\2812.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\2820.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\2830.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\2850.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\2873.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\2880.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\2881.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\2882.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\2900.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\2930.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\2950.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\2970.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\2985.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\2990.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\3020.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\3045.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\3070.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\3100.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\3125.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\3150.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\3169.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\3195.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\3225.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\3255.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\3285.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\3300.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\3325.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\3355.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\3380.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\3400.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\3420.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\3450.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\3460.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\3475.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\3490.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\3520.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\3527.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\3550.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\3560.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\3592.ini" />
|
||||
<None Include="LiveTv\TunerHosts\SatIp\ini\satellite\3594.ini" />
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,186 +0,0 @@
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using MediaBrowser.Model.Session;
|
||||
using MediaBrowser.Model.System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.Session
|
||||
{
|
||||
public class HttpSessionController : ISessionController, IDisposable
|
||||
{
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly IJsonSerializer _json;
|
||||
private readonly ISessionManager _sessionManager;
|
||||
|
||||
public SessionInfo Session { get; private set; }
|
||||
|
||||
private readonly string _postUrl;
|
||||
|
||||
public HttpSessionController(IHttpClient httpClient,
|
||||
IJsonSerializer json,
|
||||
SessionInfo session,
|
||||
string postUrl, ISessionManager sessionManager)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_json = json;
|
||||
Session = session;
|
||||
_postUrl = postUrl;
|
||||
_sessionManager = sessionManager;
|
||||
}
|
||||
|
||||
public void OnActivity()
|
||||
{
|
||||
}
|
||||
|
||||
private string PostUrl
|
||||
{
|
||||
get
|
||||
{
|
||||
return string.Format("http://{0}{1}", Session.RemoteEndPoint, _postUrl);
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsSessionActive
|
||||
{
|
||||
get
|
||||
{
|
||||
return (DateTime.UtcNow - Session.LastActivityDate).TotalMinutes <= 10;
|
||||
}
|
||||
}
|
||||
|
||||
public bool SupportsMediaControl
|
||||
{
|
||||
get { return true; }
|
||||
}
|
||||
|
||||
private Task SendMessage(string name, CancellationToken cancellationToken)
|
||||
{
|
||||
return SendMessage(name, new Dictionary<string, string>(), cancellationToken);
|
||||
}
|
||||
|
||||
private async Task SendMessage(string name,
|
||||
Dictionary<string, string> args,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var url = PostUrl + "/" + name + ToQueryString(args);
|
||||
|
||||
await _httpClient.Post(new HttpRequestOptions
|
||||
{
|
||||
Url = url,
|
||||
CancellationToken = cancellationToken,
|
||||
BufferContent = false
|
||||
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public Task SendSessionEndedNotification(SessionInfoDto sessionInfo, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
public Task SendPlaybackStartNotification(SessionInfoDto sessionInfo, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
public Task SendPlaybackStoppedNotification(SessionInfoDto sessionInfo, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
public Task SendPlayCommand(PlayRequest command, CancellationToken cancellationToken)
|
||||
{
|
||||
var dict = new Dictionary<string, string>();
|
||||
|
||||
dict["ItemIds"] = string.Join(",", command.ItemIds);
|
||||
|
||||
if (command.StartPositionTicks.HasValue)
|
||||
{
|
||||
dict["StartPositionTicks"] = command.StartPositionTicks.Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
return SendMessage(command.PlayCommand.ToString(), dict, cancellationToken);
|
||||
}
|
||||
|
||||
public Task SendPlaystateCommand(PlaystateRequest command, CancellationToken cancellationToken)
|
||||
{
|
||||
var args = new Dictionary<string, string>();
|
||||
|
||||
if (command.Command == PlaystateCommand.Seek)
|
||||
{
|
||||
if (!command.SeekPositionTicks.HasValue)
|
||||
{
|
||||
throw new ArgumentException("SeekPositionTicks cannot be null");
|
||||
}
|
||||
|
||||
args["SeekPositionTicks"] = command.SeekPositionTicks.Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
return SendMessage(command.Command.ToString(), args, cancellationToken);
|
||||
}
|
||||
|
||||
public Task SendLibraryUpdateInfo(LibraryUpdateInfo info, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
public Task SendRestartRequiredNotification(SystemInfo info, CancellationToken cancellationToken)
|
||||
{
|
||||
return SendMessage("RestartRequired", cancellationToken);
|
||||
}
|
||||
|
||||
public Task SendUserDataChangeInfo(UserDataChangeInfo info, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
public Task SendServerShutdownNotification(CancellationToken cancellationToken)
|
||||
{
|
||||
return SendMessage("ServerShuttingDown", cancellationToken);
|
||||
}
|
||||
|
||||
public Task SendServerRestartNotification(CancellationToken cancellationToken)
|
||||
{
|
||||
return SendMessage("ServerRestarting", cancellationToken);
|
||||
}
|
||||
|
||||
public Task SendGeneralCommand(GeneralCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
return SendMessage(command.Name, command.Arguments, cancellationToken);
|
||||
}
|
||||
|
||||
public Task SendMessage<T>(string name, T data, CancellationToken cancellationToken)
|
||||
{
|
||||
// Not supported or needed right now
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
private string ToQueryString(Dictionary<string, string> nvc)
|
||||
{
|
||||
var array = (from item in nvc
|
||||
select string.Format("{0}={1}", WebUtility.UrlEncode(item.Key), WebUtility.UrlEncode(item.Value)))
|
||||
.ToArray();
|
||||
|
||||
var args = string.Join("&", array);
|
||||
|
||||
if (string.IsNullOrEmpty(args))
|
||||
{
|
||||
return args;
|
||||
}
|
||||
|
||||
return "?" + args;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,485 +0,0 @@
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Events;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using MediaBrowser.Model.Session;
|
||||
using System;
|
||||
using System.Collections.Specialized;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Model.Services;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.Session
|
||||
{
|
||||
/// <summary>
|
||||
/// Class SessionWebSocketListener
|
||||
/// </summary>
|
||||
public class SessionWebSocketListener : IWebSocketListener, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The _true task result
|
||||
/// </summary>
|
||||
private readonly Task _trueTaskResult = Task.FromResult(true);
|
||||
|
||||
/// <summary>
|
||||
/// The _session manager
|
||||
/// </summary>
|
||||
private readonly ISessionManager _sessionManager;
|
||||
|
||||
/// <summary>
|
||||
/// The _logger
|
||||
/// </summary>
|
||||
private readonly ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// The _dto service
|
||||
/// </summary>
|
||||
private readonly IJsonSerializer _json;
|
||||
|
||||
private readonly IHttpServer _httpServer;
|
||||
private readonly IServerManager _serverManager;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SessionWebSocketListener" /> class.
|
||||
/// </summary>
|
||||
/// <param name="sessionManager">The session manager.</param>
|
||||
/// <param name="logManager">The log manager.</param>
|
||||
/// <param name="json">The json.</param>
|
||||
/// <param name="httpServer">The HTTP server.</param>
|
||||
/// <param name="serverManager">The server manager.</param>
|
||||
public SessionWebSocketListener(ISessionManager sessionManager, ILogManager logManager, IJsonSerializer json, IHttpServer httpServer, IServerManager serverManager)
|
||||
{
|
||||
_sessionManager = sessionManager;
|
||||
_logger = logManager.GetLogger(GetType().Name);
|
||||
_json = json;
|
||||
_httpServer = httpServer;
|
||||
_serverManager = serverManager;
|
||||
httpServer.WebSocketConnecting += _httpServer_WebSocketConnecting;
|
||||
serverManager.WebSocketConnected += _serverManager_WebSocketConnected;
|
||||
}
|
||||
|
||||
async void _serverManager_WebSocketConnected(object sender, GenericEventArgs<IWebSocketConnection> e)
|
||||
{
|
||||
var session = await GetSession(e.Argument.QueryString, e.Argument.RemoteEndPoint).ConfigureAwait(false);
|
||||
|
||||
if (session != null)
|
||||
{
|
||||
var controller = session.SessionController as WebSocketController;
|
||||
|
||||
if (controller == null)
|
||||
{
|
||||
controller = new WebSocketController(session, _logger, _sessionManager);
|
||||
}
|
||||
|
||||
controller.AddWebSocket(e.Argument);
|
||||
|
||||
session.SessionController = controller;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Warn("Unable to determine session based on url: {0}", e.Argument.Url);
|
||||
}
|
||||
}
|
||||
|
||||
async void _httpServer_WebSocketConnecting(object sender, WebSocketConnectingEventArgs e)
|
||||
{
|
||||
//var token = e.QueryString["api_key"];
|
||||
//if (!string.IsNullOrWhiteSpace(token))
|
||||
//{
|
||||
// try
|
||||
// {
|
||||
// var session = await GetSession(e.QueryString, e.Endpoint).ConfigureAwait(false);
|
||||
|
||||
// if (session == null)
|
||||
// {
|
||||
// e.AllowConnection = false;
|
||||
// }
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// _logger.ErrorException("Error getting session info", ex);
|
||||
// }
|
||||
//}
|
||||
}
|
||||
|
||||
private Task<SessionInfo> GetSession(QueryParamCollection queryString, string remoteEndpoint)
|
||||
{
|
||||
if (queryString == null)
|
||||
{
|
||||
throw new ArgumentNullException("queryString");
|
||||
}
|
||||
|
||||
var token = queryString["api_key"];
|
||||
if (string.IsNullOrWhiteSpace(token))
|
||||
{
|
||||
return Task.FromResult<SessionInfo>(null);
|
||||
}
|
||||
var deviceId = queryString["deviceId"];
|
||||
return _sessionManager.GetSessionByAuthenticationToken(token, deviceId, remoteEndpoint);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_httpServer.WebSocketConnecting -= _httpServer_WebSocketConnecting;
|
||||
_serverManager.WebSocketConnected -= _serverManager_WebSocketConnected;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes the message.
|
||||
/// </summary>
|
||||
/// <param name="message">The message.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public Task ProcessMessage(WebSocketMessageInfo message)
|
||||
{
|
||||
if (string.Equals(message.MessageType, "Identity", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
ProcessIdentityMessage(message);
|
||||
}
|
||||
else if (string.Equals(message.MessageType, "Context", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
ProcessContextMessage(message);
|
||||
}
|
||||
else if (string.Equals(message.MessageType, "PlaybackStart", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
OnPlaybackStart(message);
|
||||
}
|
||||
else if (string.Equals(message.MessageType, "PlaybackProgress", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
OnPlaybackProgress(message);
|
||||
}
|
||||
else if (string.Equals(message.MessageType, "PlaybackStopped", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
OnPlaybackStopped(message);
|
||||
}
|
||||
else if (string.Equals(message.MessageType, "ReportPlaybackStart", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
ReportPlaybackStart(message);
|
||||
}
|
||||
else if (string.Equals(message.MessageType, "ReportPlaybackProgress", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
ReportPlaybackProgress(message);
|
||||
}
|
||||
else if (string.Equals(message.MessageType, "ReportPlaybackStopped", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
ReportPlaybackStopped(message);
|
||||
}
|
||||
|
||||
return _trueTaskResult;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes the identity message.
|
||||
/// </summary>
|
||||
/// <param name="message">The message.</param>
|
||||
private async void ProcessIdentityMessage(WebSocketMessageInfo message)
|
||||
{
|
||||
_logger.Debug("Received Identity message: " + message.Data);
|
||||
|
||||
var vals = message.Data.Split('|');
|
||||
|
||||
if (vals.Length < 3)
|
||||
{
|
||||
_logger.Error("Client sent invalid identity message.");
|
||||
return;
|
||||
}
|
||||
|
||||
var client = vals[0];
|
||||
var deviceId = vals[1];
|
||||
var version = vals[2];
|
||||
var deviceName = vals.Length > 3 ? vals[3] : string.Empty;
|
||||
|
||||
var session = _sessionManager.GetSession(deviceId, client, version);
|
||||
|
||||
if (session == null && !string.IsNullOrEmpty(deviceName))
|
||||
{
|
||||
_logger.Debug("Logging session activity");
|
||||
|
||||
session = await _sessionManager.LogSessionActivity(client, version, deviceId, deviceName, message.Connection.RemoteEndPoint, null).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (session != null)
|
||||
{
|
||||
var controller = session.SessionController as WebSocketController;
|
||||
|
||||
if (controller == null)
|
||||
{
|
||||
controller = new WebSocketController(session, _logger, _sessionManager);
|
||||
}
|
||||
|
||||
controller.AddWebSocket(message.Connection);
|
||||
|
||||
session.SessionController = controller;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Warn("Unable to determine session based on identity message: {0}", message.Data);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes the context message.
|
||||
/// </summary>
|
||||
/// <param name="message">The message.</param>
|
||||
private void ProcessContextMessage(WebSocketMessageInfo message)
|
||||
{
|
||||
var session = GetSessionFromMessage(message);
|
||||
|
||||
if (session != null)
|
||||
{
|
||||
var vals = message.Data.Split('|');
|
||||
|
||||
var itemId = vals[1];
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(itemId))
|
||||
{
|
||||
_sessionManager.ReportNowViewingItem(session.Id, itemId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the session from message.
|
||||
/// </summary>
|
||||
/// <param name="message">The message.</param>
|
||||
/// <returns>SessionInfo.</returns>
|
||||
private SessionInfo GetSessionFromMessage(WebSocketMessageInfo message)
|
||||
{
|
||||
var result = _sessionManager.Sessions.FirstOrDefault(i =>
|
||||
{
|
||||
var controller = i.SessionController as WebSocketController;
|
||||
|
||||
if (controller != null)
|
||||
{
|
||||
if (controller.Sockets.Any(s => s.Id == message.Connection.Id))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
});
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
_logger.Error("Unable to find session based on web socket message");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
|
||||
|
||||
/// <summary>
|
||||
/// Reports the playback start.
|
||||
/// </summary>
|
||||
/// <param name="message">The message.</param>
|
||||
private void OnPlaybackStart(WebSocketMessageInfo message)
|
||||
{
|
||||
_logger.Debug("Received PlaybackStart message");
|
||||
|
||||
var session = GetSessionFromMessage(message);
|
||||
|
||||
if (session != null && session.UserId.HasValue)
|
||||
{
|
||||
var vals = message.Data.Split('|');
|
||||
|
||||
var itemId = vals[0];
|
||||
|
||||
var queueableMediaTypes = string.Empty;
|
||||
var canSeek = true;
|
||||
|
||||
if (vals.Length > 1)
|
||||
{
|
||||
canSeek = string.Equals(vals[1], "true", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
if (vals.Length > 2)
|
||||
{
|
||||
queueableMediaTypes = vals[2];
|
||||
}
|
||||
|
||||
var info = new PlaybackStartInfo
|
||||
{
|
||||
CanSeek = canSeek,
|
||||
ItemId = itemId,
|
||||
SessionId = session.Id,
|
||||
QueueableMediaTypes = queueableMediaTypes.Split(',').ToList()
|
||||
};
|
||||
|
||||
if (vals.Length > 3)
|
||||
{
|
||||
info.MediaSourceId = vals[3];
|
||||
}
|
||||
|
||||
if (vals.Length > 4 && !string.IsNullOrWhiteSpace(vals[4]))
|
||||
{
|
||||
info.AudioStreamIndex = int.Parse(vals[4], _usCulture);
|
||||
}
|
||||
|
||||
if (vals.Length > 5 && !string.IsNullOrWhiteSpace(vals[5]))
|
||||
{
|
||||
info.SubtitleStreamIndex = int.Parse(vals[5], _usCulture);
|
||||
}
|
||||
|
||||
_sessionManager.OnPlaybackStart(info);
|
||||
}
|
||||
}
|
||||
|
||||
private void ReportPlaybackStart(WebSocketMessageInfo message)
|
||||
{
|
||||
_logger.Debug("Received ReportPlaybackStart message");
|
||||
|
||||
var session = GetSessionFromMessage(message);
|
||||
|
||||
if (session != null && session.UserId.HasValue)
|
||||
{
|
||||
var info = _json.DeserializeFromString<PlaybackStartInfo>(message.Data);
|
||||
|
||||
info.SessionId = session.Id;
|
||||
|
||||
_sessionManager.OnPlaybackStart(info);
|
||||
}
|
||||
}
|
||||
|
||||
private void ReportPlaybackProgress(WebSocketMessageInfo message)
|
||||
{
|
||||
//_logger.Debug("Received ReportPlaybackProgress message");
|
||||
|
||||
var session = GetSessionFromMessage(message);
|
||||
|
||||
if (session != null && session.UserId.HasValue)
|
||||
{
|
||||
var info = _json.DeserializeFromString<PlaybackProgressInfo>(message.Data);
|
||||
|
||||
info.SessionId = session.Id;
|
||||
|
||||
_sessionManager.OnPlaybackProgress(info);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reports the playback progress.
|
||||
/// </summary>
|
||||
/// <param name="message">The message.</param>
|
||||
private void OnPlaybackProgress(WebSocketMessageInfo message)
|
||||
{
|
||||
var session = GetSessionFromMessage(message);
|
||||
|
||||
if (session != null && session.UserId.HasValue)
|
||||
{
|
||||
var vals = message.Data.Split('|');
|
||||
|
||||
var itemId = vals[0];
|
||||
|
||||
long? positionTicks = null;
|
||||
|
||||
if (vals.Length > 1)
|
||||
{
|
||||
long pos;
|
||||
|
||||
if (long.TryParse(vals[1], out pos))
|
||||
{
|
||||
positionTicks = pos;
|
||||
}
|
||||
}
|
||||
|
||||
var isPaused = vals.Length > 2 && string.Equals(vals[2], "true", StringComparison.OrdinalIgnoreCase);
|
||||
var isMuted = vals.Length > 3 && string.Equals(vals[3], "true", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
var info = new PlaybackProgressInfo
|
||||
{
|
||||
ItemId = itemId,
|
||||
PositionTicks = positionTicks,
|
||||
IsMuted = isMuted,
|
||||
IsPaused = isPaused,
|
||||
SessionId = session.Id
|
||||
};
|
||||
|
||||
if (vals.Length > 4)
|
||||
{
|
||||
info.MediaSourceId = vals[4];
|
||||
}
|
||||
|
||||
if (vals.Length > 5 && !string.IsNullOrWhiteSpace(vals[5]))
|
||||
{
|
||||
info.VolumeLevel = int.Parse(vals[5], _usCulture);
|
||||
}
|
||||
|
||||
if (vals.Length > 5 && !string.IsNullOrWhiteSpace(vals[6]))
|
||||
{
|
||||
info.AudioStreamIndex = int.Parse(vals[6], _usCulture);
|
||||
}
|
||||
|
||||
if (vals.Length > 7 && !string.IsNullOrWhiteSpace(vals[7]))
|
||||
{
|
||||
info.SubtitleStreamIndex = int.Parse(vals[7], _usCulture);
|
||||
}
|
||||
|
||||
_sessionManager.OnPlaybackProgress(info);
|
||||
}
|
||||
}
|
||||
|
||||
private void ReportPlaybackStopped(WebSocketMessageInfo message)
|
||||
{
|
||||
_logger.Debug("Received ReportPlaybackStopped message");
|
||||
|
||||
var session = GetSessionFromMessage(message);
|
||||
|
||||
if (session != null && session.UserId.HasValue)
|
||||
{
|
||||
var info = _json.DeserializeFromString<PlaybackStopInfo>(message.Data);
|
||||
|
||||
info.SessionId = session.Id;
|
||||
|
||||
_sessionManager.OnPlaybackStopped(info);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reports the playback stopped.
|
||||
/// </summary>
|
||||
/// <param name="message">The message.</param>
|
||||
private void OnPlaybackStopped(WebSocketMessageInfo message)
|
||||
{
|
||||
_logger.Debug("Received PlaybackStopped message");
|
||||
|
||||
var session = GetSessionFromMessage(message);
|
||||
|
||||
if (session != null && session.UserId.HasValue)
|
||||
{
|
||||
var vals = message.Data.Split('|');
|
||||
|
||||
var itemId = vals[0];
|
||||
|
||||
long? positionTicks = null;
|
||||
|
||||
if (vals.Length > 1)
|
||||
{
|
||||
long pos;
|
||||
|
||||
if (long.TryParse(vals[1], out pos))
|
||||
{
|
||||
positionTicks = pos;
|
||||
}
|
||||
}
|
||||
|
||||
var info = new PlaybackStopInfo
|
||||
{
|
||||
ItemId = itemId,
|
||||
PositionTicks = positionTicks,
|
||||
SessionId = session.Id
|
||||
};
|
||||
|
||||
if (vals.Length > 2)
|
||||
{
|
||||
info.MediaSourceId = vals[2];
|
||||
}
|
||||
|
||||
_sessionManager.OnPlaybackStopped(info);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,288 +0,0 @@
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using MediaBrowser.Model.Net;
|
||||
using MediaBrowser.Model.Session;
|
||||
using MediaBrowser.Model.System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.Session
|
||||
{
|
||||
public class WebSocketController : ISessionController, IDisposable
|
||||
{
|
||||
public SessionInfo Session { get; private set; }
|
||||
public IReadOnlyList<IWebSocketConnection> Sockets { get; private set; }
|
||||
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private readonly ISessionManager _sessionManager;
|
||||
|
||||
public WebSocketController(SessionInfo session, ILogger logger, ISessionManager sessionManager)
|
||||
{
|
||||
Session = session;
|
||||
_logger = logger;
|
||||
_sessionManager = sessionManager;
|
||||
Sockets = new List<IWebSocketConnection>();
|
||||
}
|
||||
|
||||
private bool HasOpenSockets
|
||||
{
|
||||
get { return GetActiveSockets().Any(); }
|
||||
}
|
||||
|
||||
public bool SupportsMediaControl
|
||||
{
|
||||
get { return HasOpenSockets; }
|
||||
}
|
||||
|
||||
private bool _isActive;
|
||||
private DateTime _lastActivityDate;
|
||||
public bool IsSessionActive
|
||||
{
|
||||
get
|
||||
{
|
||||
if (HasOpenSockets)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
//return false;
|
||||
return _isActive && (DateTime.UtcNow - _lastActivityDate).TotalMinutes <= 10;
|
||||
}
|
||||
}
|
||||
|
||||
public void OnActivity()
|
||||
{
|
||||
_isActive = true;
|
||||
_lastActivityDate = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
private IEnumerable<IWebSocketConnection> GetActiveSockets()
|
||||
{
|
||||
return Sockets
|
||||
.OrderByDescending(i => i.LastActivityDate)
|
||||
.Where(i => i.State == WebSocketState.Open);
|
||||
}
|
||||
|
||||
public void AddWebSocket(IWebSocketConnection connection)
|
||||
{
|
||||
var sockets = Sockets.ToList();
|
||||
sockets.Add(connection);
|
||||
|
||||
Sockets = sockets;
|
||||
|
||||
connection.Closed += connection_Closed;
|
||||
}
|
||||
|
||||
void connection_Closed(object sender, EventArgs e)
|
||||
{
|
||||
if (!GetActiveSockets().Any())
|
||||
{
|
||||
_isActive = false;
|
||||
|
||||
try
|
||||
{
|
||||
_sessionManager.ReportSessionEnded(Session.Id);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error reporting session ended.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IWebSocketConnection GetActiveSocket()
|
||||
{
|
||||
var socket = GetActiveSockets()
|
||||
.FirstOrDefault();
|
||||
|
||||
if (socket == null)
|
||||
{
|
||||
throw new InvalidOperationException("The requested session does not have an open web socket.");
|
||||
}
|
||||
|
||||
return socket;
|
||||
}
|
||||
|
||||
public Task SendPlayCommand(PlayRequest command, CancellationToken cancellationToken)
|
||||
{
|
||||
return SendMessageInternal(new WebSocketMessage<PlayRequest>
|
||||
{
|
||||
MessageType = "Play",
|
||||
Data = command
|
||||
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
public Task SendPlaystateCommand(PlaystateRequest command, CancellationToken cancellationToken)
|
||||
{
|
||||
return SendMessageInternal(new WebSocketMessage<PlaystateRequest>
|
||||
{
|
||||
MessageType = "Playstate",
|
||||
Data = command
|
||||
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
public Task SendLibraryUpdateInfo(LibraryUpdateInfo info, CancellationToken cancellationToken)
|
||||
{
|
||||
return SendMessagesInternal(new WebSocketMessage<LibraryUpdateInfo>
|
||||
{
|
||||
MessageType = "LibraryChanged",
|
||||
Data = info
|
||||
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends the restart required message.
|
||||
/// </summary>
|
||||
/// <param name="info">The information.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public Task SendRestartRequiredNotification(SystemInfo info, CancellationToken cancellationToken)
|
||||
{
|
||||
return SendMessagesInternal(new WebSocketMessage<SystemInfo>
|
||||
{
|
||||
MessageType = "RestartRequired",
|
||||
Data = info
|
||||
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Sends the user data change info.
|
||||
/// </summary>
|
||||
/// <param name="info">The info.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public Task SendUserDataChangeInfo(UserDataChangeInfo info, CancellationToken cancellationToken)
|
||||
{
|
||||
return SendMessagesInternal(new WebSocketMessage<UserDataChangeInfo>
|
||||
{
|
||||
MessageType = "UserDataChanged",
|
||||
Data = info
|
||||
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends the server shutdown notification.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public Task SendServerShutdownNotification(CancellationToken cancellationToken)
|
||||
{
|
||||
return SendMessagesInternal(new WebSocketMessage<string>
|
||||
{
|
||||
MessageType = "ServerShuttingDown",
|
||||
Data = string.Empty
|
||||
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends the server restart notification.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public Task SendServerRestartNotification(CancellationToken cancellationToken)
|
||||
{
|
||||
return SendMessagesInternal(new WebSocketMessage<string>
|
||||
{
|
||||
MessageType = "ServerRestarting",
|
||||
Data = string.Empty
|
||||
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
public Task SendGeneralCommand(GeneralCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
return SendMessageInternal(new WebSocketMessage<GeneralCommand>
|
||||
{
|
||||
MessageType = "GeneralCommand",
|
||||
Data = command
|
||||
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
public Task SendSessionEndedNotification(SessionInfoDto sessionInfo, CancellationToken cancellationToken)
|
||||
{
|
||||
return SendMessagesInternal(new WebSocketMessage<SessionInfoDto>
|
||||
{
|
||||
MessageType = "SessionEnded",
|
||||
Data = sessionInfo
|
||||
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
public Task SendPlaybackStartNotification(SessionInfoDto sessionInfo, CancellationToken cancellationToken)
|
||||
{
|
||||
return SendMessagesInternal(new WebSocketMessage<SessionInfoDto>
|
||||
{
|
||||
MessageType = "PlaybackStart",
|
||||
Data = sessionInfo
|
||||
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
public Task SendPlaybackStoppedNotification(SessionInfoDto sessionInfo, CancellationToken cancellationToken)
|
||||
{
|
||||
return SendMessagesInternal(new WebSocketMessage<SessionInfoDto>
|
||||
{
|
||||
MessageType = "PlaybackStopped",
|
||||
Data = sessionInfo
|
||||
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
public Task SendMessage<T>(string name, T data, CancellationToken cancellationToken)
|
||||
{
|
||||
return SendMessagesInternal(new WebSocketMessage<T>
|
||||
{
|
||||
Data = data,
|
||||
MessageType = name
|
||||
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
private Task SendMessageInternal<T>(WebSocketMessage<T> message, CancellationToken cancellationToken)
|
||||
{
|
||||
var socket = GetActiveSocket();
|
||||
|
||||
return socket.SendAsync(message, cancellationToken);
|
||||
}
|
||||
|
||||
private Task SendMessagesInternal<T>(WebSocketMessage<T> message, CancellationToken cancellationToken)
|
||||
{
|
||||
var tasks = GetActiveSockets().Select(i => Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await i.SendAsync(message, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error sending web socket message", ex);
|
||||
}
|
||||
|
||||
}, cancellationToken));
|
||||
|
||||
return Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var socket in Sockets.ToList())
|
||||
{
|
||||
socket.Closed -= connection_Closed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user