From d596053ec7830d89a83723b0ae2f8439c6319f6f Mon Sep 17 00:00:00 2001
From: Luke Pulverenti <luke.pulverenti@gmail.com>
Date: Sun, 25 Sep 2016 14:39:13 -0400
Subject: [PATCH] rework live stream handling

---
 MediaBrowser.Api/ApiEntryPoint.cs             |  39 ++++-
 MediaBrowser.Api/LiveTv/LiveTvService.cs      |  33 +++-
 .../Playback/Hls/BaseHlsService.cs            |  44 +++--
 .../Playback/Hls/DynamicHlsService.cs         |   7 +-
 .../BaseProgressiveStreamingService.cs        |  23 ++-
 .../Progressive/ProgressiveStreamWriter.cs    |   4 +-
 MediaBrowser.Api/Playback/StreamState.cs      |  10 +-
 .../LiveTv/ILiveTvManager.cs                  |  18 +-
 MediaBrowser.Controller/LiveTv/ITunerHost.cs  |   2 +-
 MediaBrowser.Controller/LiveTv/LiveStream.cs  |  30 ++++
 .../MediaBrowser.Controller.csproj            |   1 +
 .../IO/LibraryMonitor.cs                      |   8 +-
 .../Library/LibraryManager.cs                 |  20 ++-
 .../LiveTv/EmbyTV/DirectRecorder.cs           |  18 +-
 .../LiveTv/EmbyTV/EmbyTV.cs                   | 100 ++++++-----
 .../LiveTv/EmbyTV/EncodedRecorder.cs          |  84 +---------
 .../LiveTv/TunerHosts/BaseTunerHost.cs        | 145 ++++++----------
 .../TunerHosts/HdHomerun/HdHomerunHost.cs     |  33 +++-
 .../HdHomerun/HdHomerunLiveStream.cs          | 156 ++++++++++++++++++
 .../LiveTv/TunerHosts/M3UTunerHost.cs         |  13 +-
 .../LiveTv/TunerHosts/SatIp/SatIpHost.cs      |  10 +-
 ...MediaBrowser.Server.Implementations.csproj |   1 +
 .../MediaBrowser.WebDashboard.csproj          |  18 +-
 MediaBrowser.XbmcMetadata/EntryPoint.cs       |  10 ++
 24 files changed, 520 insertions(+), 307 deletions(-)
 create mode 100644 MediaBrowser.Controller/LiveTv/LiveStream.cs
 create mode 100644 MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunLiveStream.cs

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);
+        /// <summary>
+        /// The active transcoding jobs
+        /// </summary>
+        private readonly List<TranscodingJob> _activeTranscodingJobs = new List<TranscodingJob>();
+
+        private readonly Dictionary<string, SemaphoreSlim> _transcodingLocks =
+            new Dictionary<string, SemaphoreSlim>();
 
         /// <summary>
         /// Initializes a new instance of the <see cref="ApiEntryPoint" /> 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
             }
         }
 
-        /// <summary>
-        /// The active transcoding jobs
-        /// </summary>
-        private readonly List<TranscodingJob> _activeTranscodingJobs = new List<TranscodingJob>();
-
         /// <summary>
         /// Called when [transcode beginning].
         /// </summary>
@@ -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<string, string>(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<string, string>(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<string, string> 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
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task{SeriesTimerInfoDto}.</returns>
         Task<SeriesTimerInfoDto> GetNewTimerDefaults(string programId, CancellationToken cancellationToken);
-        
+
         /// <summary>
         /// Deletes the recording.
         /// </summary>
@@ -51,7 +51,7 @@ namespace MediaBrowser.Controller.LiveTv
         /// <param name="recording">The recording.</param>
         /// <returns>Task.</returns>
         Task DeleteRecording(BaseItem recording);
-        
+
         /// <summary>
         /// Cancels the timer.
         /// </summary>
@@ -83,7 +83,7 @@ namespace MediaBrowser.Controller.LiveTv
         /// <param name="user">The user.</param>
         /// <returns>Task{RecordingInfoDto}.</returns>
         Task<BaseItemDto> GetRecording(string id, DtoOptions options, CancellationToken cancellationToken, User user = null);
-        
+
         /// <summary>
         /// Gets the timer.
         /// </summary>
@@ -125,14 +125,14 @@ namespace MediaBrowser.Controller.LiveTv
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task{QueryResult{SeriesTimerInfoDto}}.</returns>
         Task<QueryResult<SeriesTimerInfoDto>> GetSeriesTimers(SeriesTimerQuery query, CancellationToken cancellationToken);
-        
+
         /// <summary>
         /// Gets the channel.
         /// </summary>
         /// <param name="id">The identifier.</param>
         /// <returns>Channel.</returns>
         LiveTvChannel GetInternalChannel(string id);
-        
+
         /// <summary>
         /// Gets the recording.
         /// </summary>
@@ -157,7 +157,7 @@ namespace MediaBrowser.Controller.LiveTv
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task{StreamResponseInfo}.</returns>
         Task<MediaSourceInfo> GetChannelStream(string id, string mediaSourceId, CancellationToken cancellationToken);
-        
+
         /// <summary>
         /// Gets the program.
         /// </summary>
@@ -331,8 +331,8 @@ namespace MediaBrowser.Controller.LiveTv
         /// <param name="fields">The fields.</param>
         /// <param name="user">The user.</param>
         /// <returns>Task.</returns>
-        Task AddInfoToProgramDto(List<Tuple<BaseItem,BaseItemDto>> programs, List<ItemFields> fields, User user = null);
-      
+        Task AddInfoToProgramDto(List<Tuple<BaseItem, BaseItemDto>> programs, List<ItemFields> fields, User user = null);
+
         /// <summary>
         /// Saves the tuner host.
         /// </summary>
@@ -395,7 +395,7 @@ namespace MediaBrowser.Controller.LiveTv
         Task<List<ChannelInfo>> GetChannelsForListingsProvider(string id, CancellationToken cancellationToken);
         Task<List<ChannelInfo>> GetChannelsFromListingsProviderData(string id, CancellationToken cancellationToken);
 
-        List<IListingsProvider> ListingProviders { get;}
+        List<IListingsProvider> ListingProviders { get; }
 
         event EventHandler<GenericEventArgs<TimerEventInfo>> SeriesTimerCancelled;
         event EventHandler<GenericEventArgs<TimerEventInfo>> 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
         /// <param name="streamId">The stream identifier.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task&lt;MediaSourceInfo&gt;.</returns>
-        Task<Tuple<MediaSourceInfo,SemaphoreSlim>> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken);
+        Task<LiveStream> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken);
         /// <summary>
         /// Gets the channel stream media sources.
         /// </summary>
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 @@
     <Compile Include="Library\UserDataSaveEventArgs.cs" />
     <Compile Include="LiveTv\IListingsProvider.cs" />
     <Compile Include="LiveTv\ITunerHost.cs" />
+    <Compile Include="LiveTv\LiveStream.cs" />
     <Compile Include="LiveTv\RecordingGroup.cs" />
     <Compile Include="LiveTv\RecordingStatusChangedEventArgs.cs" />
     <Compile Include="LiveTv\ILiveTvRecording.cs" />
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<string> _alwaysIgnoreSubstrings = new List<string>
         {
             // 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<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;
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<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,19 +1073,24 @@ 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
+                {
+                    await CloseLiveStream(liveStream.Id, CancellationToken.None).ConfigureAwait(false);
+                }
+                catch (Exception ex)
                 {
-                    semaphore.Release();
+                    _logger.ErrorException("Error closing live stream", ex);
                 }
+            }
 
-                _libraryManager.UnRegisterIgnoredPath(recordPath);
-                _libraryMonitor.ReportFileSystemChangeComplete(recordPath, true);
+            _libraryManager.UnRegisterIgnoredPath(recordPath);
+            _libraryMonitor.ReportFileSystemChangeComplete(recordPath, true);
 
-                ActiveRecordingInfo removed;
-                _activeRecordings.TryRemove(timer.Id, out removed);
-            }
+            ActiveRecordingInfo removed;
+            _activeRecordings.TryRemove(timer.Id, out removed);
 
             if (recordingStatus == 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);
-
-                return;
-            }
+            var durationToken = new CancellationTokenSource(duration);
+            cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token;
 
-            var tempfile = Path.Combine(_appPaths.TranscodingTempPath, Guid.NewGuid().ToString("N") + ".ts");
+            await RecordFromFile(mediaSource, mediaSource.Path, targetFile, false, duration, onStarted, cancellationToken).ConfigureAwait(false);
 
-            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<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();
-
-                var hostsWithChannel = new List<TunerHostInfo>();
+                throw new FileNotFoundException();
+            }
 
-                foreach (var host in hosts)
-                {
-                    if (string.IsNullOrWhiteSpace(streamId))
-                    {
-                        try
-                        {
-                            var channels = await GetChannels(host, true, cancellationToken).ConfigureAwait(false);
+            var hosts = GetTunerHosts();
 
-                            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;
-                    }
-                }
+            var hostsWithChannel = new List<TunerHostInfo>();
 
-                foreach (var host in hostsWithChannel)
+            foreach (var host in hosts)
+            {
+                if (string.IsNullOrWhiteSpace(streamId))
                 {
-                    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)
-                        {
-                            if (!await IsAvailable(host, channelId, cancellationToken).ConfigureAwait(false))
-                            {
-                                Logger.Error("Tuner is not currently available");
-                                resourcePool.Release();
-                                continue;
-                            }
-                        }
-
-                        var stream = await GetChannelStream(host, channelId, streamId, cancellationToken).ConfigureAwait(false);
+                        var channels = await GetChannels(host, true, cancellationToken).ConfigureAwait(false);
 
-                        if (EnableMediaProbing)
+                        if (channels.Any(i => string.Equals(i.Id, channelId, StringComparison.OrdinalIgnoreCase)))
                         {
-                            await AddMediaInfo(stream, false, resourcePool, cancellationToken).ConfigureAwait(false);
+                            hostsWithChannel.Add(host);
                         }
-
-                        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)
-        {
-            return _semaphoreLocks.GetOrAdd(url, key => new SemaphoreSlim(1, 1));
-        }
-
-        private async Task AddMediaInfo(MediaSourceInfo mediaSource, bool isAudio, SemaphoreSlim resourcePool, CancellationToken cancellationToken)
+        private async Task AddMediaInfo(LiveStream stream, bool isAudio, CancellationToken cancellationToken)
         {
-            await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
+            //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<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)
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<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);
+            }
+        }
+    }
+}
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<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 };
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<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)
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 @@
     <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" />
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 @@
     <Content Include="dashboard-ui\camerauploadsettings.html">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
+    <Content Include="dashboard-ui\components\accessschedule\accessschedule.js">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
+    <Content Include="dashboard-ui\components\accessschedule\accessschedule.template.html">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
     <Content Include="dashboard-ui\components\appfooter\appfooter.css">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
@@ -437,15 +443,6 @@
     <Content Include="dashboard-ui\scripts\sections.js">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
-    <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\jqm.collapsible.css">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\jqm.collapsible.js">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\jqm.controlgroup.css">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
     <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\jqm.listview.css">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
@@ -470,9 +467,6 @@
     <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\jqm.widget.js">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
-    <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\jquery.mobile.custom.theme.css">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
     <Content Include="dashboard-ui\thirdparty\paper-button-style.css">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
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);