rework live stream handling

This commit is contained in:
Luke Pulverenti
2016-09-25 14:39:13 -04:00
parent 48d7f686eb
commit d596053ec7
24 changed files with 523 additions and 310 deletions

View File

@@ -43,16 +43,14 @@ namespace MediaBrowser.Server.Implementations.IO
// WMC temp recording directories that will constantly be written to
"TempRec",
"TempSBE",
"@eaDir",
"eaDir",
"#recycle"
"TempSBE"
};
private readonly IReadOnlyList<string> _alwaysIgnoreSubstrings = new List<string>
{
// Synology
"@eaDir",
"eaDir",
"#recycle",
".wd_tv",
".actors"
};

View File

@@ -2803,6 +2803,17 @@ namespace MediaBrowser.Server.Implementations.Library
}
}
private bool ValidateNetworkPath(string path)
{
if (Environment.OSVersion.Platform == PlatformID.Win32NT || !path.StartsWith("\\\\", StringComparison.OrdinalIgnoreCase))
{
return Directory.Exists(path);
}
// Without native support for unc, we cannot validate this when running under mono
return true;
}
private const string ShortcutFileExtension = ".mblink";
private const string ShortcutFileSearch = "*" + ShortcutFileExtension;
public void AddMediaPath(string virtualFolderName, MediaPathInfo pathInfo)
@@ -2829,12 +2840,7 @@ namespace MediaBrowser.Server.Implementations.Library
throw new DirectoryNotFoundException("The path does not exist.");
}
if (!string.IsNullOrWhiteSpace(pathInfo.NetworkPath) && !_fileSystem.DirectoryExists(pathInfo.NetworkPath))
{
throw new DirectoryNotFoundException("The network path does not exist.");
}
if (!string.IsNullOrWhiteSpace(pathInfo.NetworkPath) && !_fileSystem.DirectoryExists(pathInfo.NetworkPath))
if (!string.IsNullOrWhiteSpace(pathInfo.NetworkPath) && !ValidateNetworkPath(pathInfo.NetworkPath))
{
throw new DirectoryNotFoundException("The network path does not exist.");
}
@@ -2877,7 +2883,7 @@ namespace MediaBrowser.Server.Implementations.Library
throw new ArgumentNullException("path");
}
if (!string.IsNullOrWhiteSpace(pathInfo.NetworkPath) && !_fileSystem.DirectoryExists(pathInfo.NetworkPath))
if (!string.IsNullOrWhiteSpace(pathInfo.NetworkPath) && !ValidateNetworkPath(pathInfo.NetworkPath))
{
throw new DirectoryNotFoundException("The network path does not exist.");
}

View File

@@ -69,11 +69,17 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
}
private const int BufferSize = 81920;
public static async Task CopyUntilCancelled(Stream source, Stream target, CancellationToken cancellationToken)
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, cancellationToken).ConfigureAwait(false);
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);
@@ -85,7 +91,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
}
}
private static async Task<int> CopyToAsyncInternal(Stream source, Stream destination, Int32 bufferSize, CancellationToken cancellationToken)
private static async Task<int> CopyToAsyncInternal(Stream source, Stream destination, Int32 bufferSize, Action onStarted, CancellationToken cancellationToken)
{
byte[] buffer = new byte[bufferSize];
int bytesRead;
@@ -96,6 +102,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
totalBytesRead += bytesRead;
if (onStarted != null)
{
onStarted();
}
onStarted = null;
}
return totalBytesRead;

View File

@@ -746,33 +746,17 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
throw new NotImplementedException();
}
private readonly SemaphoreSlim _liveStreamsSemaphore = new SemaphoreSlim(1, 1);
private readonly Dictionary<string, LiveStream> _liveStreams = new Dictionary<string, LiveStream>();
public async Task<MediaSourceInfo> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken)
{
_logger.Info("Streaming Channel " + channelId);
var result = await GetChannelStreamInternal(channelId, streamId, cancellationToken).ConfigureAwait(false);
foreach (var hostInstance in _liveTvManager.TunerHosts)
{
try
{
var result = await hostInstance.GetChannelStream(channelId, streamId, cancellationToken).ConfigureAwait(false);
result.Item2.Release();
return result.Item1;
}
catch (FileNotFoundException)
{
}
catch (Exception e)
{
_logger.ErrorException("Error getting channel stream", e);
}
}
throw new ApplicationException("Tuner not found.");
return result.Item1.PublicMediaSource;
}
private async Task<Tuple<MediaSourceInfo, ITunerHost, SemaphoreSlim>> GetChannelStreamInternal(string channelId, string streamId, CancellationToken cancellationToken)
private async Task<Tuple<LiveStream, ITunerHost>> GetChannelStreamInternal(string channelId, string streamId, CancellationToken cancellationToken)
{
_logger.Info("Streaming Channel " + channelId);
@@ -782,7 +766,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
{
var result = await hostInstance.GetChannelStream(channelId, streamId, cancellationToken).ConfigureAwait(false);
return new Tuple<MediaSourceInfo, ITunerHost, SemaphoreSlim>(result.Item1, hostInstance, result.Item2);
await _liveStreamsSemaphore.WaitAsync().ConfigureAwait(false);
_liveStreams[result.Id] = result;
_liveStreamsSemaphore.Release();
return new Tuple<LiveStream, ITunerHost>(result, hostInstance);
}
catch (FileNotFoundException)
{
@@ -823,9 +811,31 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
throw new NotImplementedException();
}
public Task CloseLiveStream(string id, CancellationToken cancellationToken)
public async Task CloseLiveStream(string id, CancellationToken cancellationToken)
{
return Task.FromResult(0);
await _liveStreamsSemaphore.WaitAsync().ConfigureAwait(false);
try
{
LiveStream stream;
if (_liveStreams.TryGetValue(id, out stream))
{
_liveStreams.Remove(id);
try
{
await stream.Close().ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.ErrorException("Error closing live stream", ex);
}
}
}
finally
{
_liveStreamsSemaphore.Release();
}
}
public Task RecordLiveStream(string id, CancellationToken cancellationToken)
@@ -999,15 +1009,17 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
string seriesPath = null;
var recordPath = GetRecordingPath(timer, out seriesPath);
var recordingStatus = RecordingStatus.New;
var isResourceOpen = false;
SemaphoreSlim semaphore = null;
LiveStream liveStream = null;
try
{
var result = await GetChannelStreamInternal(timer.ChannelId, null, CancellationToken.None).ConfigureAwait(false);
isResourceOpen = true;
semaphore = result.Item3;
var mediaStreamInfo = result.Item1;
var allMediaSources = await GetChannelStreamMediaSources(timer.ChannelId, CancellationToken.None).ConfigureAwait(false);
var liveStreamInfo = await GetChannelStreamInternal(timer.ChannelId, allMediaSources[0].Id, CancellationToken.None).ConfigureAwait(false);
liveStream = liveStreamInfo.Item1;
var mediaStreamInfo = liveStreamInfo.Item1.PublicMediaSource;
var tunerHost = liveStreamInfo.Item2;
// HDHR doesn't seem to release the tuner right away after first probing with ffmpeg
//await Task.Delay(3000, cancellationToken).ConfigureAwait(false);
@@ -1034,13 +1046,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
timer.Status = RecordingStatus.InProgress;
_timerProvider.AddOrUpdate(timer, false);
result.Item3.Release();
isResourceOpen = false;
SaveNfo(timer, recordPath, seriesPath);
};
var pathWithDuration = result.Item2.ApplyDuration(mediaStreamInfo.Path, duration);
var pathWithDuration = tunerHost.ApplyDuration(mediaStreamInfo.Path, duration);
// If it supports supplying duration via url
if (!string.Equals(pathWithDuration, mediaStreamInfo.Path, StringComparison.OrdinalIgnoreCase))
@@ -1064,20 +1073,25 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
_logger.ErrorException("Error recording to {0}", ex, recordPath);
recordingStatus = RecordingStatus.Error;
}
finally
if (liveStream != null)
{
if (isResourceOpen && semaphore != null)
try
{
semaphore.Release();
await CloseLiveStream(liveStream.Id, CancellationToken.None).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.ErrorException("Error closing live stream", ex);
}
_libraryManager.UnRegisterIgnoredPath(recordPath);
_libraryMonitor.ReportFileSystemChangeComplete(recordPath, true);
ActiveRecordingInfo removed;
_activeRecordings.TryRemove(timer.Id, out removed);
}
_libraryManager.UnRegisterIgnoredPath(recordPath);
_libraryMonitor.ReportFileSystemChangeComplete(recordPath, true);
ActiveRecordingInfo removed;
_activeRecordings.TryRemove(timer.Id, out removed);
if (recordingStatus == RecordingStatus.Completed)
{
timer.Status = RecordingStatus.Completed;

View File

@@ -68,18 +68,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
public async Task Record(MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
{
if (mediaSource.Path.IndexOf("m3u8", StringComparison.OrdinalIgnoreCase) != -1)
{
await RecordWithoutTempFile(mediaSource, targetFile, duration, onStarted, cancellationToken)
.ConfigureAwait(false);
var durationToken = new CancellationTokenSource(duration);
cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token;
return;
}
await RecordFromFile(mediaSource, mediaSource.Path, targetFile, false, duration, onStarted, cancellationToken).ConfigureAwait(false);
var tempfile = Path.Combine(_appPaths.TranscodingTempPath, Guid.NewGuid().ToString("N") + ".ts");
await RecordWithTempFile(mediaSource, tempfile, targetFile, duration, onStarted, cancellationToken)
.ConfigureAwait(false);
_logger.Info("Recording completed to file {0}", targetFile);
}
private async void DeleteTempFile(string path)
@@ -108,76 +102,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
}
}
private async Task RecordWithoutTempFile(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, false, duration, onStarted, cancellationToken).ConfigureAwait(false);
_logger.Info("Recording completed to file {0}", targetFile);
}
private async Task RecordWithTempFile(MediaSourceInfo mediaSource, string tempFile, 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");
Directory.CreateDirectory(Path.GetDirectoryName(tempFile));
using (var output = _fileSystem.GetFileStream(tempFile, FileMode.Create, FileAccess.Write, FileShare.Read))
{
//onStarted();
_logger.Info("Copying recording stream to file {0}", tempFile);
var bufferMs = 5000;
if (mediaSource.RunTimeTicks.HasValue)
{
// The media source already has a fixed duration
// But add another stop 1 minute later just in case the recording gets stuck for any reason
var durationToken = new CancellationTokenSource(duration.Add(TimeSpan.FromMinutes(1)));
cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token;
}
else
{
// The media source if infinite so we need to handle stopping ourselves
var durationToken = new CancellationTokenSource(duration.Add(TimeSpan.FromMilliseconds(bufferMs)));
cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token;
}
var tempFileTask = DirectRecorder.CopyUntilCancelled(response.Content, output, cancellationToken);
// Give the temp file a little time to build up
await Task.Delay(bufferMs, cancellationToken).ConfigureAwait(false);
var recordTask = Task.Run(() => RecordFromFile(mediaSource, tempFile, targetFile, true, duration, onStarted, cancellationToken), CancellationToken.None);
try
{
await tempFileTask.ConfigureAwait(false);
}
catch (OperationCanceledException)
{
}
await recordTask.ConfigureAwait(false);
}
}
_logger.Info("Recording completed to file {0}", targetFile);
}
private Task RecordFromFile(MediaSourceInfo mediaSource, string inputFile, string targetFile, bool deleteInputFileAfterCompletion, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
{
_targetPath = targetFile;

View File

@@ -10,6 +10,7 @@ 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;
@@ -18,7 +19,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
{
public abstract class BaseTunerHost
{
protected readonly IConfigurationManager Config;
protected readonly IServerConfigurationManager Config;
protected readonly ILogger Logger;
protected IJsonSerializer JsonSerializer;
protected readonly IMediaEncoder MediaEncoder;
@@ -26,7 +27,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
private readonly ConcurrentDictionary<string, ChannelCache> _channelCache =
new ConcurrentDictionary<string, ChannelCache>(StringComparer.OrdinalIgnoreCase);
protected BaseTunerHost(IConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder)
protected BaseTunerHost(IServerConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder)
{
Config = config;
Logger = logger;
@@ -125,12 +126,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
foreach (var host in hostsWithChannel)
{
var resourcePool = GetLock(host.Url);
Logger.Debug("GetChannelStreamMediaSources - Waiting on tuner resource pool");
await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
Logger.Debug("GetChannelStreamMediaSources - Unlocked resource pool");
try
{
// Check to make sure the tuner is available
@@ -156,93 +151,63 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
{
Logger.Error("Error opening tuner", ex);
}
finally
{
resourcePool.Release();
}
}
}
return new List<MediaSourceInfo>();
}
protected abstract Task<MediaSourceInfo> GetChannelStream(TunerHostInfo tuner, string channelId, string streamId, CancellationToken cancellationToken);
protected abstract Task<LiveStream> GetChannelStream(TunerHostInfo tuner, string channelId, string streamId, CancellationToken cancellationToken);
public async Task<Tuple<MediaSourceInfo, SemaphoreSlim>> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken)
public async Task<LiveStream> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken)
{
if (IsValidChannelId(channelId))
if (!IsValidChannelId(channelId))
{
var hosts = GetTunerHosts();
throw new FileNotFoundException();
}
var hostsWithChannel = new List<TunerHostInfo>();
var hosts = GetTunerHosts();
foreach (var host in hosts)
var hostsWithChannel = new List<TunerHostInfo>();
foreach (var host in hosts)
{
if (string.IsNullOrWhiteSpace(streamId))
{
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)
{
var resourcePool = GetLock(host.Url);
Logger.Debug("GetChannelStream - Waiting on tuner resource pool");
await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
Logger.Debug("GetChannelStream - Unlocked resource pool");
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 a streamId is specified then availibility has already been checked in GetChannelStreamMediaSources
if (string.IsNullOrWhiteSpace(streamId) && hostsWithChannel.Count > 1)
var channels = await GetChannels(host, true, cancellationToken).ConfigureAwait(false);
if (channels.Any(i => string.Equals(i.Id, channelId, StringComparison.OrdinalIgnoreCase)))
{
if (!await IsAvailable(host, channelId, cancellationToken).ConfigureAwait(false))
{
Logger.Error("Tuner is not currently available");
resourcePool.Release();
continue;
}
hostsWithChannel.Add(host);
}
var stream = await GetChannelStream(host, channelId, streamId, cancellationToken).ConfigureAwait(false);
if (EnableMediaProbing)
{
await AddMediaInfo(stream, false, resourcePool, cancellationToken).ConfigureAwait(false);
}
return new Tuple<MediaSourceInfo, SemaphoreSlim>(stream, resourcePool);
}
catch (Exception ex)
{
Logger.Error("Error opening tuner", ex);
resourcePool.Release();
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;
}
}
else
foreach (var host in hostsWithChannel)
{
throw new FileNotFoundException();
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();
@@ -268,37 +233,23 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
protected abstract Task<bool> IsAvailableInternal(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken);
/// <summary>
/// The _semaphoreLocks
/// </summary>
private readonly ConcurrentDictionary<string, SemaphoreSlim> _semaphoreLocks = new ConcurrentDictionary<string, SemaphoreSlim>(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Gets the lock.
/// </summary>
/// <param name="url">The filename.</param>
/// <returns>System.Object.</returns>
private SemaphoreSlim GetLock(string url)
private async Task AddMediaInfo(LiveStream stream, bool isAudio, CancellationToken cancellationToken)
{
return _semaphoreLocks.GetOrAdd(url, key => new SemaphoreSlim(1, 1));
}
//await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
private async Task AddMediaInfo(MediaSourceInfo mediaSource, bool isAudio, SemaphoreSlim resourcePool, CancellationToken cancellationToken)
{
await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
//try
//{
// await AddMediaInfoInternal(mediaSource, isAudio, cancellationToken).ConfigureAwait(false);
try
{
await AddMediaInfoInternal(mediaSource, isAudio, cancellationToken).ConfigureAwait(false);
// // Leave the resource locked. it will be released upstream
//}
//catch (Exception)
//{
// // Release the resource if there's some kind of failure.
// resourcePool.Release();
// Leave the resource locked. it will be released upstream
}
catch (Exception)
{
// Release the resource if there's some kind of failure.
resourcePool.Release();
throw;
}
// throw;
//}
}
private async Task AddMediaInfoInternal(MediaSourceInfo mediaSource, bool isAudio, CancellationToken cancellationToken)

View File

@@ -14,7 +14,10 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using CommonIO;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Net;
@@ -24,11 +27,15 @@ 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(IConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IHttpClient httpClient)
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
@@ -355,6 +362,13 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
url += "?transcode=" + profile;
}
var id = profile;
if (string.IsNullOrWhiteSpace(id))
{
id = "native";
}
id += "_" + url.GetMD5().ToString("N");
var mediaSource = new MediaSourceInfo
{
Path = url,
@@ -387,9 +401,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
RequiresClosing = false,
BufferMs = 0,
Container = "ts",
Id = profile,
SupportsDirectPlay = true,
SupportsDirectStream = false,
Id = id,
SupportsDirectPlay = false,
SupportsDirectStream = true,
SupportsTranscoding = true
};
@@ -452,9 +466,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
return channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase);
}
protected override async Task<MediaSourceInfo> GetChannelStream(TunerHostInfo info, string channelId, string streamId, CancellationToken cancellationToken)
protected override async Task<LiveStream> GetChannelStream(TunerHostInfo info, string channelId, string streamId, CancellationToken cancellationToken)
{
Logger.Info("GetChannelStream: channel id: {0}. stream id: {1}", channelId, streamId ?? string.Empty);
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))
{
@@ -462,7 +478,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
}
var hdhrId = GetHdHrIdFromChannelId(channelId);
return await GetMediaSource(info, hdhrId, streamId).ConfigureAwait(false);
var mediaSource = await GetMediaSource(info, hdhrId, profile).ConfigureAwait(false);
var liveStream = new HdHomerunLiveStream(mediaSource, _fileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost);
return liveStream;
}
public async Task Validate(TunerHostInfo info)

View File

@@ -0,0 +1,156 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using CommonIO;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Logging;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Server.Implementations.LiveTv.EmbyTV;
namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
public class HdHomerunLiveStream : LiveStream
{
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();
public HdHomerunLiveStream(MediaSourceInfo mediaSource, IFileSystem fileSystem, IHttpClient httpClient, ILogger logger, IServerApplicationPaths appPaths, IServerApplicationHost appHost)
: base(mediaSource)
{
_fileSystem = fileSystem;
_httpClient = httpClient;
_logger = logger;
_appPaths = appPaths;
_appHost = appHost;
}
public override async Task Open(CancellationToken openCancellationToken)
{
_liveStreamCancellationTokenSource.Token.ThrowIfCancellationRequested();
var mediaSource = OriginalMediaSource;
var url = mediaSource.Path;
var tempFile = Path.Combine(_appPaths.TranscodingTempPath, Guid.NewGuid().ToString("N") + ".ts");
Directory.CreateDirectory(Path.GetDirectoryName(tempFile));
_logger.Info("Opening HDHR Live stream from {0} to {1}", url, tempFile);
var output = _fileSystem.GetFileStream(tempFile, FileMode.Create, FileAccess.Write, FileShare.Read);
var taskCompletionSource = new TaskCompletionSource<bool>();
StartStreamingToTempFile(output, tempFile, url, taskCompletionSource, _liveStreamCancellationTokenSource.Token);
await taskCompletionSource.Task.ConfigureAwait(false);
PublicMediaSource.Path = _appHost.GetLocalApiUrl("localhost") + "/LiveTv/LiveStreamFiles/" + Path.GetFileNameWithoutExtension(tempFile) + "/stream.ts";
PublicMediaSource.Protocol = MediaProtocol.Http;
}
public override Task Close()
{
_liveStreamCancellationTokenSource.Cancel();
return base.Close();
}
private async Task StartStreamingToTempFile(Stream outputStream, string tempFilePath, string url, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
{
await Task.Run(async () =>
{
using (outputStream)
{
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 DirectRecorder.CopyUntilCancelled");
Action onStarted = null;
if (isFirstAttempt)
{
onStarted = () => openTaskCompletionSource.TrySetResult(true);
}
await DirectRecorder.CopyUntilCancelled(response.Content, outputStream, 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;
}
}
await Task.Delay(5000).ConfigureAwait(false);
DeleteTempFile(tempFilePath);
}).ConfigureAwait(false);
}
private async void DeleteTempFile(string path)
{
for (var i = 0; i < 10; i++)
{
try
{
File.Delete(path);
return;
}
catch (FileNotFoundException)
{
return;
}
catch (DirectoryNotFoundException)
{
return;
}
catch (Exception ex)
{
_logger.ErrorException("Error deleting temp file {0}", ex, path);
}
await Task.Delay(1000).ConfigureAwait(false);
}
}
}
}

View File

@@ -13,8 +13,10 @@ using System.Threading;
using System.Threading.Tasks;
using CommonIO;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Server.Implementations.LiveTv.EmbyTV;
namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
{
@@ -23,7 +25,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
private readonly IFileSystem _fileSystem;
private readonly IHttpClient _httpClient;
public M3UTunerHost(IConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IHttpClient httpClient)
public M3UTunerHost(IServerConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IHttpClient httpClient)
: base(config, logger, jsonSerializer, mediaEncoder)
{
_fileSystem = fileSystem;
@@ -63,11 +65,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
return Task.FromResult(list);
}
protected override async Task<MediaSourceInfo> GetChannelStream(TunerHostInfo info, string channelId, string streamId, CancellationToken cancellationToken)
protected override async Task<LiveStream> GetChannelStream(TunerHostInfo info, string channelId, string streamId, CancellationToken cancellationToken)
{
var sources = await GetChannelStreamMediaSources(info, channelId, cancellationToken).ConfigureAwait(false);
return sources.First();
var liveStream = new LiveStream(sources.First());
return liveStream;
}
public async Task Validate(TunerHostInfo info)
@@ -136,7 +139,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
RequiresOpening = false,
RequiresClosing = false,
ReadAtNativeFramerate = false
ReadAtNativeFramerate = false,
Id = channel.Path.GetMD5().ToString("N")
};
return new List<MediaSourceInfo> { mediaSource };

View File

@@ -8,6 +8,7 @@ using CommonIO;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Dto;
@@ -16,6 +17,7 @@ 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
{
@@ -24,7 +26,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp
private readonly IFileSystem _fileSystem;
private readonly IHttpClient _httpClient;
public SatIpHost(IConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IHttpClient httpClient)
public SatIpHost(IServerConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IHttpClient httpClient)
: base(config, logger, jsonSerializer, mediaEncoder)
{
_fileSystem = fileSystem;
@@ -113,11 +115,13 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp
return new List<MediaSourceInfo>();
}
protected override async Task<MediaSourceInfo> GetChannelStream(TunerHostInfo tuner, string channelId, string streamId, CancellationToken cancellationToken)
protected override async Task<LiveStream> GetChannelStream(TunerHostInfo tuner, string channelId, string streamId, CancellationToken cancellationToken)
{
var sources = await GetChannelStreamMediaSources(tuner, channelId, cancellationToken).ConfigureAwait(false);
return sources.First();
var liveStream = new LiveStream(sources.First());
return liveStream;
}
protected override async Task<bool> IsAvailableInternal(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken)

View File

@@ -241,6 +241,7 @@
<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\ProgramImageProvider.cs" />