diff --git a/MediaBrowser.Api/ApiEntryPoint.cs b/MediaBrowser.Api/ApiEntryPoint.cs
index 214fb74882..2f5b9e1e06 100644
--- a/MediaBrowser.Api/ApiEntryPoint.cs
+++ b/MediaBrowser.Api/ApiEntryPoint.cs
@@ -8,6 +8,7 @@ using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Logging;
using MediaBrowser.Model.Session;
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
@@ -44,7 +45,13 @@ namespace MediaBrowser.Api
private readonly IFileSystem _fileSystem;
private readonly IMediaSourceManager _mediaSourceManager;
- public readonly SemaphoreSlim TranscodingStartLock = new SemaphoreSlim(1, 1);
+ ///
+ /// The active transcoding jobs
+ ///
+ private readonly List _activeTranscodingJobs = new List();
+
+ private readonly Dictionary _transcodingLocks =
+ new Dictionary();
///
/// Initializes a new instance of the class.
@@ -67,6 +74,21 @@ namespace MediaBrowser.Api
_sessionManager.PlaybackStart += _sessionManager_PlaybackStart;
}
+ public SemaphoreSlim GetTranscodingLock(string outputPath)
+ {
+ lock (_transcodingLocks)
+ {
+ SemaphoreSlim result;
+ if (!_transcodingLocks.TryGetValue(outputPath, out result))
+ {
+ result = new SemaphoreSlim(1, 1);
+ _transcodingLocks[outputPath] = result;
+ }
+
+ return result;
+ }
+ }
+
private void _sessionManager_PlaybackStart(object sender, PlaybackProgressEventArgs e)
{
if (!string.IsNullOrWhiteSpace(e.PlaySessionId))
@@ -148,11 +170,6 @@ namespace MediaBrowser.Api
}
}
- ///
- /// The active transcoding jobs
- ///
- private readonly List _activeTranscodingJobs = new List();
-
///
/// Called when [transcode beginning].
///
@@ -258,6 +275,11 @@ namespace MediaBrowser.Api
}
}
+ lock (_transcodingLocks)
+ {
+ _transcodingLocks.Remove(path);
+ }
+
if (!string.IsNullOrWhiteSpace(state.Request.DeviceId))
{
_sessionManager.ClearTranscodingInfo(state.Request.DeviceId);
@@ -497,6 +519,11 @@ namespace MediaBrowser.Api
}
}
+ lock (_transcodingLocks)
+ {
+ _transcodingLocks.Remove(job.Path);
+ }
+
lock (job.ProcessLock)
{
if (job.TranscodingThrottler != null)
diff --git a/MediaBrowser.Api/LiveTv/LiveTvService.cs b/MediaBrowser.Api/LiveTv/LiveTvService.cs
index 3ad0ec1bae..a5f8fce6e0 100644
--- a/MediaBrowser.Api/LiveTv/LiveTvService.cs
+++ b/MediaBrowser.Api/LiveTv/LiveTvService.cs
@@ -12,9 +12,13 @@ using ServiceStack;
using System;
using System.Collections.Generic;
using System.Globalization;
+using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using CommonIO;
+using MediaBrowser.Api.Playback.Progressive;
+using MediaBrowser.Controller.Configuration;
namespace MediaBrowser.Api.LiveTv
{
@@ -613,16 +617,24 @@ namespace MediaBrowser.Api.LiveTv
}
+ [Route("/LiveTv/LiveStreamFiles/{Id}/stream.{Container}", "GET", Summary = "Gets a live tv channel")]
+ public class GetLiveStreamFile
+ {
+ public string Id { get; set; }
+ public string Container { get; set; }
+ }
+
public class LiveTvService : BaseApiService
{
private readonly ILiveTvManager _liveTvManager;
private readonly IUserManager _userManager;
- private readonly IConfigurationManager _config;
+ private readonly IServerConfigurationManager _config;
private readonly IHttpClient _httpClient;
private readonly ILibraryManager _libraryManager;
private readonly IDtoService _dtoService;
+ private readonly IFileSystem _fileSystem;
- public LiveTvService(ILiveTvManager liveTvManager, IUserManager userManager, IConfigurationManager config, IHttpClient httpClient, ILibraryManager libraryManager, IDtoService dtoService)
+ public LiveTvService(ILiveTvManager liveTvManager, IUserManager userManager, IServerConfigurationManager config, IHttpClient httpClient, ILibraryManager libraryManager, IDtoService dtoService, IFileSystem fileSystem)
{
_liveTvManager = liveTvManager;
_userManager = userManager;
@@ -630,6 +642,23 @@ namespace MediaBrowser.Api.LiveTv
_httpClient = httpClient;
_libraryManager = libraryManager;
_dtoService = dtoService;
+ _fileSystem = fileSystem;
+ }
+
+ public object Get(GetLiveStreamFile request)
+ {
+ var filePath = Path.Combine(_config.ApplicationPaths.TranscodingTempPath, request.Id + ".ts");
+
+ var outputHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase);
+
+ outputHeaders["Content-Type"] = MimeTypes.GetMimeType(filePath);
+
+ var streamSource = new ProgressiveFileCopier(_fileSystem, filePath, outputHeaders, null, Logger, CancellationToken.None)
+ {
+ AllowEndOfFile = false
+ };
+
+ return ResultFactory.GetAsyncStreamWriter(streamSource);
}
public object Get(GetDefaultListingProvider request)
diff --git a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs
index 761b1eb4ec..319e4bbb68 100644
--- a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs
+++ b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs
@@ -87,7 +87,8 @@ namespace MediaBrowser.Api.Playback.Hls
if (!FileSystem.FileExists(playlist))
{
- await ApiEntryPoint.Instance.TranscodingStartLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
+ var transcodingLock = ApiEntryPoint.Instance.GetTranscodingLock(playlist);
+ await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
try
{
if (!FileSystem.FileExists(playlist))
@@ -104,13 +105,13 @@ namespace MediaBrowser.Api.Playback.Hls
throw;
}
- var waitForSegments = state.SegmentLength >= 10 ? 2 : (state.SegmentLength > 3 || !isLive ? 3 : 4);
+ var waitForSegments = state.SegmentLength >= 10 ? 2 : (state.SegmentLength > 3 || !isLive ? 3 : 3);
await WaitForMinimumSegmentCount(playlist, waitForSegments, cancellationTokenSource.Token).ConfigureAwait(false);
}
}
finally
{
- ApiEntryPoint.Instance.TranscodingStartLock.Release();
+ transcodingLock.Release();
}
}
@@ -182,32 +183,41 @@ namespace MediaBrowser.Api.Playback.Hls
{
Logger.Debug("Waiting for {0} segments in {1}", segmentCount, playlist);
- while (true)
+ while (!cancellationToken.IsCancellationRequested)
{
- // Need to use FileShare.ReadWrite because we're reading the file at the same time it's being written
- using (var fileStream = GetPlaylistFileStream(playlist))
+ try
{
- using (var reader = new StreamReader(fileStream))
+ // Need to use FileShare.ReadWrite because we're reading the file at the same time it's being written
+ using (var fileStream = GetPlaylistFileStream(playlist))
{
- var count = 0;
-
- while (!reader.EndOfStream)
+ using (var reader = new StreamReader(fileStream))
{
- var line = await reader.ReadLineAsync().ConfigureAwait(false);
+ var count = 0;
- if (line.IndexOf("#EXTINF:", StringComparison.OrdinalIgnoreCase) != -1)
+ while (!reader.EndOfStream)
{
- count++;
- if (count >= segmentCount)
+ var line = await reader.ReadLineAsync().ConfigureAwait(false);
+
+ if (line.IndexOf("#EXTINF:", StringComparison.OrdinalIgnoreCase) != -1)
{
- Logger.Debug("Finished waiting for {0} segments in {1}", segmentCount, playlist);
- return;
+ count++;
+ if (count >= segmentCount)
+ {
+ Logger.Debug("Finished waiting for {0} segments in {1}", segmentCount, playlist);
+ return;
+ }
}
}
+ await Task.Delay(100, cancellationToken).ConfigureAwait(false);
}
- await Task.Delay(100, cancellationToken).ConfigureAwait(false);
}
}
+ catch (IOException)
+ {
+ // May get an error if the file is locked
+ }
+
+ await Task.Delay(50, cancellationToken).ConfigureAwait(false);
}
}
diff --git a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs
index 9cd55528d3..d4ddbd7c5e 100644
--- a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs
+++ b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs
@@ -171,14 +171,15 @@ namespace MediaBrowser.Api.Playback.Hls
return await GetSegmentResult(state, playlistPath, segmentPath, requestedIndex, job, cancellationToken).ConfigureAwait(false);
}
- await ApiEntryPoint.Instance.TranscodingStartLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
+ var transcodingLock = ApiEntryPoint.Instance.GetTranscodingLock(playlistPath);
+ await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
var released = false;
try
{
if (FileSystem.FileExists(segmentPath))
{
job = ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
- ApiEntryPoint.Instance.TranscodingStartLock.Release();
+ transcodingLock.Release();
released = true;
return await GetSegmentResult(state, playlistPath, segmentPath, requestedIndex, job, cancellationToken).ConfigureAwait(false);
}
@@ -242,7 +243,7 @@ namespace MediaBrowser.Api.Playback.Hls
{
if (!released)
{
- ApiEntryPoint.Instance.TranscodingStartLock.Release();
+ transcodingLock.Release();
}
}
diff --git a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs
index b8cb6b14f0..a683191094 100644
--- a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs
+++ b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs
@@ -17,6 +17,7 @@ using System.IO;
using System.Threading;
using System.Threading.Tasks;
using CommonIO;
+using ServiceStack;
namespace MediaBrowser.Api.Playback.Progressive
{
@@ -129,6 +130,23 @@ namespace MediaBrowser.Api.Playback.Progressive
using (state)
{
+ if (state.MediaPath.IndexOf("/livestreamfiles/", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ var parts = state.MediaPath.Split('/');
+ var filename = parts[parts.Length - 2] + Path.GetExtension(parts[parts.Length - 1]);
+ var filePath = Path.Combine(ServerConfigurationManager.ApplicationPaths.TranscodingTempPath, filename);
+
+ var outputHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase);
+
+ outputHeaders["Content-Type"] = MimeTypes.GetMimeType(filePath);
+
+ var streamSource = new ProgressiveFileCopier(FileSystem, filePath, outputHeaders, null, Logger, CancellationToken.None)
+ {
+ AllowEndOfFile = false
+ };
+ return ResultFactory.GetAsyncStreamWriter(streamSource);
+ }
+
return await GetStaticRemoteStreamResult(state, responseHeaders, isHeadRequest, cancellationTokenSource)
.ConfigureAwait(false);
}
@@ -345,7 +363,8 @@ namespace MediaBrowser.Api.Playback.Progressive
return streamResult;
}
- await ApiEntryPoint.Instance.TranscodingStartLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
+ var transcodingLock = ApiEntryPoint.Instance.GetTranscodingLock(outputPath);
+ await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
try
{
TranscodingJob job;
@@ -376,7 +395,7 @@ namespace MediaBrowser.Api.Playback.Progressive
}
finally
{
- ApiEntryPoint.Instance.TranscodingStartLock.Release();
+ transcodingLock.Release();
}
}
diff --git a/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs b/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs
index 0a9a446412..80b5e357df 100644
--- a/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs
+++ b/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs
@@ -24,6 +24,8 @@ namespace MediaBrowser.Api.Playback.Progressive
private long _bytesWritten = 0;
+ public bool AllowEndOfFile = true;
+
public ProgressiveFileCopier(IFileSystem fileSystem, string path, Dictionary outputHeaders, TranscodingJob job, ILogger logger, CancellationToken cancellationToken)
{
_fileSystem = fileSystem;
@@ -50,7 +52,7 @@ namespace MediaBrowser.Api.Playback.Progressive
using (var fs = _fileSystem.GetFileStream(_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true))
{
- while (eofCount < 15)
+ while (eofCount < 15 || !AllowEndOfFile)
{
var bytesRead = await CopyToAsyncInternal(fs, outputStream, BufferSize, _cancellationToken).ConfigureAwait(false);
diff --git a/MediaBrowser.Api/Playback/StreamState.cs b/MediaBrowser.Api/Playback/StreamState.cs
index 109aa85de4..ef0282abcd 100644
--- a/MediaBrowser.Api/Playback/StreamState.cs
+++ b/MediaBrowser.Api/Playback/StreamState.cs
@@ -73,10 +73,6 @@ namespace MediaBrowser.Api.Playback
{
get
{
- if (!RunTimeTicks.HasValue)
- {
- return 6;
- }
if (string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
{
var userAgent = UserAgent ?? string.Empty;
@@ -92,12 +88,16 @@ namespace MediaBrowser.Api.Playback
return 10;
}
+ if (!RunTimeTicks.HasValue)
+ {
+ return 3;
+ }
return 6;
}
if (!RunTimeTicks.HasValue)
{
- return 6;
+ return 3;
}
return 3;
}
diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
index a8e42749b0..41c5dbdbbe 100644
--- a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
+++ b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
@@ -37,7 +37,7 @@ namespace MediaBrowser.Controller.LiveTv
/// The cancellation token.
/// Task{SeriesTimerInfoDto}.
Task GetNewTimerDefaults(string programId, CancellationToken cancellationToken);
-
+
///
/// Deletes the recording.
///
@@ -51,7 +51,7 @@ namespace MediaBrowser.Controller.LiveTv
/// The recording.
/// Task.
Task DeleteRecording(BaseItem recording);
-
+
///
/// Cancels the timer.
///
@@ -83,7 +83,7 @@ namespace MediaBrowser.Controller.LiveTv
/// The user.
/// Task{RecordingInfoDto}.
Task GetRecording(string id, DtoOptions options, CancellationToken cancellationToken, User user = null);
-
+
///
/// Gets the timer.
///
@@ -125,14 +125,14 @@ namespace MediaBrowser.Controller.LiveTv
/// The cancellation token.
/// Task{QueryResult{SeriesTimerInfoDto}}.
Task> GetSeriesTimers(SeriesTimerQuery query, CancellationToken cancellationToken);
-
+
///
/// Gets the channel.
///
/// The identifier.
/// Channel.
LiveTvChannel GetInternalChannel(string id);
-
+
///
/// Gets the recording.
///
@@ -157,7 +157,7 @@ namespace MediaBrowser.Controller.LiveTv
/// The cancellation token.
/// Task{StreamResponseInfo}.
Task GetChannelStream(string id, string mediaSourceId, CancellationToken cancellationToken);
-
+
///
/// Gets the program.
///
@@ -331,8 +331,8 @@ namespace MediaBrowser.Controller.LiveTv
/// The fields.
/// The user.
/// Task.
- Task AddInfoToProgramDto(List> programs, List fields, User user = null);
-
+ Task AddInfoToProgramDto(List> programs, List fields, User user = null);
+
///
/// Saves the tuner host.
///
@@ -395,7 +395,7 @@ namespace MediaBrowser.Controller.LiveTv
Task> GetChannelsForListingsProvider(string id, CancellationToken cancellationToken);
Task> GetChannelsFromListingsProviderData(string id, CancellationToken cancellationToken);
- List ListingProviders { get;}
+ List ListingProviders { get; }
event EventHandler> SeriesTimerCancelled;
event EventHandler> TimerCancelled;
diff --git a/MediaBrowser.Controller/LiveTv/ITunerHost.cs b/MediaBrowser.Controller/LiveTv/ITunerHost.cs
index 1e7aa3de5a..3c8b964a2b 100644
--- a/MediaBrowser.Controller/LiveTv/ITunerHost.cs
+++ b/MediaBrowser.Controller/LiveTv/ITunerHost.cs
@@ -38,7 +38,7 @@ namespace MediaBrowser.Controller.LiveTv
/// The stream identifier.
/// The cancellation token.
/// Task<MediaSourceInfo>.
- Task> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken);
+ Task GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken);
///
/// Gets the channel stream media sources.
///
diff --git a/MediaBrowser.Controller/LiveTv/LiveStream.cs b/MediaBrowser.Controller/LiveTv/LiveStream.cs
new file mode 100644
index 0000000000..15d09d8575
--- /dev/null
+++ b/MediaBrowser.Controller/LiveTv/LiveStream.cs
@@ -0,0 +1,30 @@
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Dto;
+
+namespace MediaBrowser.Controller.LiveTv
+{
+ public class LiveStream
+ {
+ public MediaSourceInfo OriginalMediaSource { get; set; }
+ public MediaSourceInfo PublicMediaSource { get; set; }
+ public string Id { get; set; }
+
+ public LiveStream(MediaSourceInfo mediaSource)
+ {
+ OriginalMediaSource = mediaSource;
+ PublicMediaSource = mediaSource;
+ Id = mediaSource.Id;
+ }
+
+ public virtual Task Open(CancellationToken cancellationToken)
+ {
+ return Task.FromResult(true);
+ }
+
+ public virtual Task Close()
+ {
+ return Task.FromResult(true);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
index cb36afa5f7..d70fba742e 100644
--- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj
+++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
@@ -201,6 +201,7 @@
+
diff --git a/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs b/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs
index bcc4e5dcfb..76f0e6a1d9 100644
--- a/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs
+++ b/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs
@@ -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 _alwaysIgnoreSubstrings = new List
{
// Synology
- "@eaDir",
+ "eaDir",
+ "#recycle",
".wd_tv",
".actors"
};
diff --git a/MediaBrowser.Server.Implementations/Library/LibraryManager.cs b/MediaBrowser.Server.Implementations/Library/LibraryManager.cs
index 7c3196065d..b076996df2 100644
--- a/MediaBrowser.Server.Implementations/Library/LibraryManager.cs
+++ b/MediaBrowser.Server.Implementations/Library/LibraryManager.cs
@@ -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.");
}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
index 2e3edf3e94..0d043669af 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
@@ -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 CopyToAsyncInternal(Stream source, Stream destination, Int32 bufferSize, CancellationToken cancellationToken)
+ private static async Task 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;
diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
index e358f9d25c..6585e92bee 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
@@ -746,33 +746,17 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
throw new NotImplementedException();
}
+ private readonly SemaphoreSlim _liveStreamsSemaphore = new SemaphoreSlim(1, 1);
+ private readonly Dictionary _liveStreams = new Dictionary();
+
public async Task 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> GetChannelStreamInternal(string channelId, string streamId, CancellationToken cancellationToken)
+ private async Task> 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(result.Item1, hostInstance, result.Item2);
+ await _liveStreamsSemaphore.WaitAsync().ConfigureAwait(false);
+ _liveStreams[result.Id] = result;
+ _liveStreamsSemaphore.Release();
+
+ return new Tuple(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;
diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
index 4e7f637b11..f74a76e3f9 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
@@ -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;
diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
index 3f6bb140b8..6beea352a9 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
@@ -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 _channelCache =
new ConcurrentDictionary(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();
}
- protected abstract Task GetChannelStream(TunerHostInfo tuner, string channelId, string streamId, CancellationToken cancellationToken);
+ protected abstract Task GetChannelStream(TunerHostInfo tuner, string channelId, string streamId, CancellationToken cancellationToken);
- public async Task> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken)
+ public async Task GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken)
{
- if (IsValidChannelId(channelId))
+ if (!IsValidChannelId(channelId))
{
- var hosts = GetTunerHosts();
+ throw new FileNotFoundException();
+ }
- var hostsWithChannel = new List();
+ var hosts = GetTunerHosts();
- foreach (var host in hosts)
+ var hostsWithChannel = new List();
+
+ 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 {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(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 { 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 IsAvailableInternal(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken);
- ///
- /// The _semaphoreLocks
- ///
- private readonly ConcurrentDictionary _semaphoreLocks = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase);
- ///
- /// Gets the lock.
- ///
- /// The filename.
- /// System.Object.
- 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)
diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
index c5bd648cf0..b40b74436d 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
@@ -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 GetChannelStream(TunerHostInfo info, string channelId, string streamId, CancellationToken cancellationToken)
+ protected override async Task 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)
diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunLiveStream.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunLiveStream.cs
new file mode 100644
index 0000000000..6078c4a704
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunLiveStream.cs
@@ -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();
+
+ 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 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);
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
index 5c508aacd3..d9c0bb8bf1 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
@@ -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 GetChannelStream(TunerHostInfo info, string channelId, string streamId, CancellationToken cancellationToken)
+ protected override async Task 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 { mediaSource };
diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpHost.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpHost.cs
index b1e349a862..81deb29959 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpHost.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpHost.cs
@@ -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();
}
- protected override async Task GetChannelStream(TunerHostInfo tuner, string channelId, string streamId, CancellationToken cancellationToken)
+ protected override async Task 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 IsAvailableInternal(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken)
diff --git a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj
index eb3da1a12e..12691a69be 100644
--- a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj
+++ b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj
@@ -241,6 +241,7 @@
+
diff --git a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj
index 00279fb050..b57416fab7 100644
--- a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj
+++ b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj
@@ -104,6 +104,12 @@
PreserveNewest
+
+ PreserveNewest
+
+
+ PreserveNewest
+
PreserveNewest
@@ -437,15 +443,6 @@
PreserveNewest
-
- PreserveNewest
-
-
- PreserveNewest
-
-
- PreserveNewest
-
PreserveNewest
@@ -470,9 +467,6 @@
PreserveNewest
-
- PreserveNewest
-
PreserveNewest
diff --git a/MediaBrowser.XbmcMetadata/EntryPoint.cs b/MediaBrowser.XbmcMetadata/EntryPoint.cs
index 0844f1f74e..bf3d3c3039 100644
--- a/MediaBrowser.XbmcMetadata/EntryPoint.cs
+++ b/MediaBrowser.XbmcMetadata/EntryPoint.cs
@@ -91,6 +91,16 @@ namespace MediaBrowser.XbmcMetadata
return;
}
+ if (!item.SupportsLocalMetadata)
+ {
+ return;
+ }
+
+ if (!item.IsSaveLocalMetadataEnabled())
+ {
+ return;
+ }
+
try
{
await _providerManager.SaveMetadata(item, updateReason, new[] { BaseNfoSaver.SaverName }).ConfigureAwait(false);