mirror of
				https://github.com/jellyfin/jellyfin.git
				synced 2025-11-03 19:17:24 -05:00 
			
		
		
		
	rework live stream handling
This commit is contained in:
		
							parent
							
								
									48d7f686eb
								
							
						
					
					
						commit
						d596053ec7
					
				@ -8,6 +8,7 @@ using MediaBrowser.Model.Configuration;
 | 
				
			|||||||
using MediaBrowser.Model.Logging;
 | 
					using MediaBrowser.Model.Logging;
 | 
				
			||||||
using MediaBrowser.Model.Session;
 | 
					using MediaBrowser.Model.Session;
 | 
				
			||||||
using System;
 | 
					using System;
 | 
				
			||||||
 | 
					using System.Collections.Concurrent;
 | 
				
			||||||
using System.Collections.Generic;
 | 
					using System.Collections.Generic;
 | 
				
			||||||
using System.Diagnostics;
 | 
					using System.Diagnostics;
 | 
				
			||||||
using System.IO;
 | 
					using System.IO;
 | 
				
			||||||
@ -44,7 +45,13 @@ namespace MediaBrowser.Api
 | 
				
			|||||||
        private readonly IFileSystem _fileSystem;
 | 
					        private readonly IFileSystem _fileSystem;
 | 
				
			||||||
        private readonly IMediaSourceManager _mediaSourceManager;
 | 
					        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>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Initializes a new instance of the <see cref="ApiEntryPoint" /> class.
 | 
					        /// Initializes a new instance of the <see cref="ApiEntryPoint" /> class.
 | 
				
			||||||
@ -67,6 +74,21 @@ namespace MediaBrowser.Api
 | 
				
			|||||||
            _sessionManager.PlaybackStart += _sessionManager_PlaybackStart;
 | 
					            _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)
 | 
					        private void _sessionManager_PlaybackStart(object sender, PlaybackProgressEventArgs e)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            if (!string.IsNullOrWhiteSpace(e.PlaySessionId))
 | 
					            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>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Called when [transcode beginning].
 | 
					        /// Called when [transcode beginning].
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
@ -258,6 +275,11 @@ namespace MediaBrowser.Api
 | 
				
			|||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            lock (_transcodingLocks)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                _transcodingLocks.Remove(path);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (!string.IsNullOrWhiteSpace(state.Request.DeviceId))
 | 
					            if (!string.IsNullOrWhiteSpace(state.Request.DeviceId))
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                _sessionManager.ClearTranscodingInfo(state.Request.DeviceId);
 | 
					                _sessionManager.ClearTranscodingInfo(state.Request.DeviceId);
 | 
				
			||||||
@ -497,6 +519,11 @@ namespace MediaBrowser.Api
 | 
				
			|||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            lock (_transcodingLocks)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                _transcodingLocks.Remove(job.Path);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            lock (job.ProcessLock)
 | 
					            lock (job.ProcessLock)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                if (job.TranscodingThrottler != null)
 | 
					                if (job.TranscodingThrottler != null)
 | 
				
			||||||
 | 
				
			|||||||
@ -12,9 +12,13 @@ using ServiceStack;
 | 
				
			|||||||
using System;
 | 
					using System;
 | 
				
			||||||
using System.Collections.Generic;
 | 
					using System.Collections.Generic;
 | 
				
			||||||
using System.Globalization;
 | 
					using System.Globalization;
 | 
				
			||||||
 | 
					using System.IO;
 | 
				
			||||||
using System.Linq;
 | 
					using System.Linq;
 | 
				
			||||||
using System.Threading;
 | 
					using System.Threading;
 | 
				
			||||||
using System.Threading.Tasks;
 | 
					using System.Threading.Tasks;
 | 
				
			||||||
 | 
					using CommonIO;
 | 
				
			||||||
 | 
					using MediaBrowser.Api.Playback.Progressive;
 | 
				
			||||||
 | 
					using MediaBrowser.Controller.Configuration;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace MediaBrowser.Api.LiveTv
 | 
					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
 | 
					    public class LiveTvService : BaseApiService
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        private readonly ILiveTvManager _liveTvManager;
 | 
					        private readonly ILiveTvManager _liveTvManager;
 | 
				
			||||||
        private readonly IUserManager _userManager;
 | 
					        private readonly IUserManager _userManager;
 | 
				
			||||||
        private readonly IConfigurationManager _config;
 | 
					        private readonly IServerConfigurationManager _config;
 | 
				
			||||||
        private readonly IHttpClient _httpClient;
 | 
					        private readonly IHttpClient _httpClient;
 | 
				
			||||||
        private readonly ILibraryManager _libraryManager;
 | 
					        private readonly ILibraryManager _libraryManager;
 | 
				
			||||||
        private readonly IDtoService _dtoService;
 | 
					        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;
 | 
					            _liveTvManager = liveTvManager;
 | 
				
			||||||
            _userManager = userManager;
 | 
					            _userManager = userManager;
 | 
				
			||||||
@ -630,6 +642,23 @@ namespace MediaBrowser.Api.LiveTv
 | 
				
			|||||||
            _httpClient = httpClient;
 | 
					            _httpClient = httpClient;
 | 
				
			||||||
            _libraryManager = libraryManager;
 | 
					            _libraryManager = libraryManager;
 | 
				
			||||||
            _dtoService = dtoService;
 | 
					            _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)
 | 
					        public object Get(GetDefaultListingProvider request)
 | 
				
			||||||
 | 
				
			|||||||
@ -87,7 +87,8 @@ namespace MediaBrowser.Api.Playback.Hls
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            if (!FileSystem.FileExists(playlist))
 | 
					            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
 | 
					                try
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                    if (!FileSystem.FileExists(playlist))
 | 
					                    if (!FileSystem.FileExists(playlist))
 | 
				
			||||||
@ -104,13 +105,13 @@ namespace MediaBrowser.Api.Playback.Hls
 | 
				
			|||||||
                            throw;
 | 
					                            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);
 | 
					                        await WaitForMinimumSegmentCount(playlist, waitForSegments, cancellationTokenSource.Token).ConfigureAwait(false);
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                finally
 | 
					                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);
 | 
					            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
 | 
					                try
 | 
				
			||||||
                using (var fileStream = GetPlaylistFileStream(playlist))
 | 
					 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                    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;
 | 
					                        using (var reader = new StreamReader(fileStream))
 | 
				
			||||||
 | 
					 | 
				
			||||||
                        while (!reader.EndOfStream)
 | 
					 | 
				
			||||||
                        {
 | 
					                        {
 | 
				
			||||||
                            var line = await reader.ReadLineAsync().ConfigureAwait(false);
 | 
					                            var count = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                            if (line.IndexOf("#EXTINF:", StringComparison.OrdinalIgnoreCase) != -1)
 | 
					                            while (!reader.EndOfStream)
 | 
				
			||||||
                            {
 | 
					                            {
 | 
				
			||||||
                                count++;
 | 
					                                var line = await reader.ReadLineAsync().ConfigureAwait(false);
 | 
				
			||||||
                                if (count >= segmentCount)
 | 
					
 | 
				
			||||||
 | 
					                                if (line.IndexOf("#EXTINF:", StringComparison.OrdinalIgnoreCase) != -1)
 | 
				
			||||||
                                {
 | 
					                                {
 | 
				
			||||||
                                    Logger.Debug("Finished waiting for {0} segments in {1}", segmentCount, playlist);
 | 
					                                    count++;
 | 
				
			||||||
                                    return;
 | 
					                                    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);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -171,14 +171,15 @@ namespace MediaBrowser.Api.Playback.Hls
 | 
				
			|||||||
                return await GetSegmentResult(state, playlistPath, segmentPath, requestedIndex, job, cancellationToken).ConfigureAwait(false);
 | 
					                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;
 | 
					            var released = false;
 | 
				
			||||||
            try
 | 
					            try
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                if (FileSystem.FileExists(segmentPath))
 | 
					                if (FileSystem.FileExists(segmentPath))
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                    job = ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
 | 
					                    job = ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
 | 
				
			||||||
                    ApiEntryPoint.Instance.TranscodingStartLock.Release();
 | 
					                    transcodingLock.Release();
 | 
				
			||||||
                    released = true;
 | 
					                    released = true;
 | 
				
			||||||
                    return await GetSegmentResult(state, playlistPath, segmentPath, requestedIndex, job, cancellationToken).ConfigureAwait(false);
 | 
					                    return await GetSegmentResult(state, playlistPath, segmentPath, requestedIndex, job, cancellationToken).ConfigureAwait(false);
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
@ -242,7 +243,7 @@ namespace MediaBrowser.Api.Playback.Hls
 | 
				
			|||||||
            {
 | 
					            {
 | 
				
			||||||
                if (!released)
 | 
					                if (!released)
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                    ApiEntryPoint.Instance.TranscodingStartLock.Release();
 | 
					                    transcodingLock.Release();
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -17,6 +17,7 @@ using System.IO;
 | 
				
			|||||||
using System.Threading;
 | 
					using System.Threading;
 | 
				
			||||||
using System.Threading.Tasks;
 | 
					using System.Threading.Tasks;
 | 
				
			||||||
using CommonIO;
 | 
					using CommonIO;
 | 
				
			||||||
 | 
					using ServiceStack;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace MediaBrowser.Api.Playback.Progressive
 | 
					namespace MediaBrowser.Api.Playback.Progressive
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
@ -129,6 +130,23 @@ namespace MediaBrowser.Api.Playback.Progressive
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
                using (state)
 | 
					                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)
 | 
					                    return await GetStaticRemoteStreamResult(state, responseHeaders, isHeadRequest, cancellationTokenSource)
 | 
				
			||||||
                                .ConfigureAwait(false);
 | 
					                                .ConfigureAwait(false);
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
@ -345,7 +363,8 @@ namespace MediaBrowser.Api.Playback.Progressive
 | 
				
			|||||||
                return streamResult;
 | 
					                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
 | 
					            try
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                TranscodingJob job;
 | 
					                TranscodingJob job;
 | 
				
			||||||
@ -376,7 +395,7 @@ namespace MediaBrowser.Api.Playback.Progressive
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
            finally
 | 
					            finally
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                ApiEntryPoint.Instance.TranscodingStartLock.Release();
 | 
					                transcodingLock.Release();
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -24,6 +24,8 @@ namespace MediaBrowser.Api.Playback.Progressive
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        private long _bytesWritten = 0;
 | 
					        private long _bytesWritten = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public bool AllowEndOfFile = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public ProgressiveFileCopier(IFileSystem fileSystem, string path, Dictionary<string, string> outputHeaders, TranscodingJob job, ILogger logger, CancellationToken cancellationToken)
 | 
					        public ProgressiveFileCopier(IFileSystem fileSystem, string path, Dictionary<string, string> outputHeaders, TranscodingJob job, ILogger logger, CancellationToken cancellationToken)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            _fileSystem = fileSystem;
 | 
					            _fileSystem = fileSystem;
 | 
				
			||||||
@ -50,7 +52,7 @@ namespace MediaBrowser.Api.Playback.Progressive
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
                using (var fs = _fileSystem.GetFileStream(_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true))
 | 
					                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);
 | 
					                        var bytesRead = await CopyToAsyncInternal(fs, outputStream, BufferSize, _cancellationToken).ConfigureAwait(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -73,10 +73,6 @@ namespace MediaBrowser.Api.Playback
 | 
				
			|||||||
        {
 | 
					        {
 | 
				
			||||||
            get
 | 
					            get
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                if (!RunTimeTicks.HasValue)
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    return 6;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                if (string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
 | 
					                if (string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                    var userAgent = UserAgent ?? string.Empty;
 | 
					                    var userAgent = UserAgent ?? string.Empty;
 | 
				
			||||||
@ -92,12 +88,16 @@ namespace MediaBrowser.Api.Playback
 | 
				
			|||||||
                        return 10;
 | 
					                        return 10;
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    if (!RunTimeTicks.HasValue)
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        return 3;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
                    return 6;
 | 
					                    return 6;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if (!RunTimeTicks.HasValue)
 | 
					                if (!RunTimeTicks.HasValue)
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                    return 6;
 | 
					                    return 3;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                return 3;
 | 
					                return 3;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
				
			|||||||
@ -331,7 +331,7 @@ namespace MediaBrowser.Controller.LiveTv
 | 
				
			|||||||
        /// <param name="fields">The fields.</param>
 | 
					        /// <param name="fields">The fields.</param>
 | 
				
			||||||
        /// <param name="user">The user.</param>
 | 
					        /// <param name="user">The user.</param>
 | 
				
			||||||
        /// <returns>Task.</returns>
 | 
					        /// <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>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Saves the tuner host.
 | 
					        /// Saves the tuner host.
 | 
				
			||||||
@ -395,7 +395,7 @@ namespace MediaBrowser.Controller.LiveTv
 | 
				
			|||||||
        Task<List<ChannelInfo>> GetChannelsForListingsProvider(string id, CancellationToken cancellationToken);
 | 
					        Task<List<ChannelInfo>> GetChannelsForListingsProvider(string id, CancellationToken cancellationToken);
 | 
				
			||||||
        Task<List<ChannelInfo>> GetChannelsFromListingsProviderData(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>> SeriesTimerCancelled;
 | 
				
			||||||
        event EventHandler<GenericEventArgs<TimerEventInfo>> TimerCancelled;
 | 
					        event EventHandler<GenericEventArgs<TimerEventInfo>> TimerCancelled;
 | 
				
			||||||
 | 
				
			|||||||
@ -38,7 +38,7 @@ namespace MediaBrowser.Controller.LiveTv
 | 
				
			|||||||
        /// <param name="streamId">The stream identifier.</param>
 | 
					        /// <param name="streamId">The stream identifier.</param>
 | 
				
			||||||
        /// <param name="cancellationToken">The cancellation token.</param>
 | 
					        /// <param name="cancellationToken">The cancellation token.</param>
 | 
				
			||||||
        /// <returns>Task<MediaSourceInfo>.</returns>
 | 
					        /// <returns>Task<MediaSourceInfo>.</returns>
 | 
				
			||||||
        Task<Tuple<MediaSourceInfo,SemaphoreSlim>> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken);
 | 
					        Task<LiveStream> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken);
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Gets the channel stream media sources.
 | 
					        /// Gets the channel stream media sources.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										30
									
								
								MediaBrowser.Controller/LiveTv/LiveStream.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								MediaBrowser.Controller/LiveTv/LiveStream.cs
									
									
									
									
									
										Normal file
									
								
							@ -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);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -201,6 +201,7 @@
 | 
				
			|||||||
    <Compile Include="Library\UserDataSaveEventArgs.cs" />
 | 
					    <Compile Include="Library\UserDataSaveEventArgs.cs" />
 | 
				
			||||||
    <Compile Include="LiveTv\IListingsProvider.cs" />
 | 
					    <Compile Include="LiveTv\IListingsProvider.cs" />
 | 
				
			||||||
    <Compile Include="LiveTv\ITunerHost.cs" />
 | 
					    <Compile Include="LiveTv\ITunerHost.cs" />
 | 
				
			||||||
 | 
					    <Compile Include="LiveTv\LiveStream.cs" />
 | 
				
			||||||
    <Compile Include="LiveTv\RecordingGroup.cs" />
 | 
					    <Compile Include="LiveTv\RecordingGroup.cs" />
 | 
				
			||||||
    <Compile Include="LiveTv\RecordingStatusChangedEventArgs.cs" />
 | 
					    <Compile Include="LiveTv\RecordingStatusChangedEventArgs.cs" />
 | 
				
			||||||
    <Compile Include="LiveTv\ILiveTvRecording.cs" />
 | 
					    <Compile Include="LiveTv\ILiveTvRecording.cs" />
 | 
				
			||||||
 | 
				
			|||||||
@ -43,16 +43,14 @@ namespace MediaBrowser.Server.Implementations.IO
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            // WMC temp recording directories that will constantly be written to
 | 
					            // WMC temp recording directories that will constantly be written to
 | 
				
			||||||
            "TempRec",
 | 
					            "TempRec",
 | 
				
			||||||
            "TempSBE",
 | 
					            "TempSBE"
 | 
				
			||||||
            "@eaDir",
 | 
					 | 
				
			||||||
            "eaDir",
 | 
					 | 
				
			||||||
            "#recycle"
 | 
					 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private readonly IReadOnlyList<string> _alwaysIgnoreSubstrings = new List<string>
 | 
					        private readonly IReadOnlyList<string> _alwaysIgnoreSubstrings = new List<string>
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            // Synology
 | 
					            // Synology
 | 
				
			||||||
            "@eaDir",
 | 
					            "eaDir",
 | 
				
			||||||
 | 
					            "#recycle",
 | 
				
			||||||
            ".wd_tv",
 | 
					            ".wd_tv",
 | 
				
			||||||
            ".actors"
 | 
					            ".actors"
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
				
			|||||||
@ -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 ShortcutFileExtension = ".mblink";
 | 
				
			||||||
        private const string ShortcutFileSearch = "*" + ShortcutFileExtension;
 | 
					        private const string ShortcutFileSearch = "*" + ShortcutFileExtension;
 | 
				
			||||||
        public void AddMediaPath(string virtualFolderName, MediaPathInfo pathInfo)
 | 
					        public void AddMediaPath(string virtualFolderName, MediaPathInfo pathInfo)
 | 
				
			||||||
@ -2829,12 +2840,7 @@ namespace MediaBrowser.Server.Implementations.Library
 | 
				
			|||||||
                throw new DirectoryNotFoundException("The path does not exist.");
 | 
					                throw new DirectoryNotFoundException("The 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.");
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (!string.IsNullOrWhiteSpace(pathInfo.NetworkPath) && !_fileSystem.DirectoryExists(pathInfo.NetworkPath))
 | 
					 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                throw new DirectoryNotFoundException("The network path does not exist.");
 | 
					                throw new DirectoryNotFoundException("The network path does not exist.");
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@ -2877,7 +2883,7 @@ namespace MediaBrowser.Server.Implementations.Library
 | 
				
			|||||||
                throw new ArgumentNullException("path");
 | 
					                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.");
 | 
					                throw new DirectoryNotFoundException("The network path does not exist.");
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
				
			|||||||
@ -69,11 +69,17 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private const int BufferSize = 81920;
 | 
					        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)
 | 
					            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;
 | 
					                //var position = fs.Position;
 | 
				
			||||||
                //_logger.Debug("Streamed {0} bytes to position {1} from file {2}", bytesRead, position, path);
 | 
					                //_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];
 | 
					            byte[] buffer = new byte[bufferSize];
 | 
				
			||||||
            int bytesRead;
 | 
					            int bytesRead;
 | 
				
			||||||
@ -96,6 +102,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
 | 
				
			|||||||
                await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
 | 
					                await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                totalBytesRead += bytesRead;
 | 
					                totalBytesRead += bytesRead;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (onStarted != null)
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    onStarted();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                onStarted = null;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return totalBytesRead;
 | 
					            return totalBytesRead;
 | 
				
			||||||
 | 
				
			|||||||
@ -746,33 +746,17 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
 | 
				
			|||||||
            throw new NotImplementedException();
 | 
					            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)
 | 
					        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)
 | 
					            return result.Item1.PublicMediaSource;
 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                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.");
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        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);
 | 
					            _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);
 | 
					                    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)
 | 
					                catch (FileNotFoundException)
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
@ -823,9 +811,31 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
 | 
				
			|||||||
            throw new NotImplementedException();
 | 
					            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)
 | 
					        public Task RecordLiveStream(string id, CancellationToken cancellationToken)
 | 
				
			||||||
@ -999,15 +1009,17 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
 | 
				
			|||||||
            string seriesPath = null;
 | 
					            string seriesPath = null;
 | 
				
			||||||
            var recordPath = GetRecordingPath(timer, out seriesPath);
 | 
					            var recordPath = GetRecordingPath(timer, out seriesPath);
 | 
				
			||||||
            var recordingStatus = RecordingStatus.New;
 | 
					            var recordingStatus = RecordingStatus.New;
 | 
				
			||||||
            var isResourceOpen = false;
 | 
					
 | 
				
			||||||
            SemaphoreSlim semaphore = null;
 | 
					            LiveStream liveStream = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            try
 | 
					            try
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                var result = await GetChannelStreamInternal(timer.ChannelId, null, CancellationToken.None).ConfigureAwait(false);
 | 
					                var allMediaSources = await GetChannelStreamMediaSources(timer.ChannelId, CancellationToken.None).ConfigureAwait(false);
 | 
				
			||||||
                isResourceOpen = true;
 | 
					
 | 
				
			||||||
                semaphore = result.Item3;
 | 
					                var liveStreamInfo = await GetChannelStreamInternal(timer.ChannelId, allMediaSources[0].Id, CancellationToken.None).ConfigureAwait(false);
 | 
				
			||||||
                var mediaStreamInfo = result.Item1;
 | 
					                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
 | 
					                // HDHR doesn't seem to release the tuner right away after first probing with ffmpeg
 | 
				
			||||||
                //await Task.Delay(3000, cancellationToken).ConfigureAwait(false);
 | 
					                //await Task.Delay(3000, cancellationToken).ConfigureAwait(false);
 | 
				
			||||||
@ -1034,13 +1046,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
 | 
				
			|||||||
                    timer.Status = RecordingStatus.InProgress;
 | 
					                    timer.Status = RecordingStatus.InProgress;
 | 
				
			||||||
                    _timerProvider.AddOrUpdate(timer, false);
 | 
					                    _timerProvider.AddOrUpdate(timer, false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    result.Item3.Release();
 | 
					 | 
				
			||||||
                    isResourceOpen = false;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    SaveNfo(timer, recordPath, seriesPath);
 | 
					                    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 it supports supplying duration via url
 | 
				
			||||||
                if (!string.Equals(pathWithDuration, mediaStreamInfo.Path, StringComparison.OrdinalIgnoreCase))
 | 
					                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);
 | 
					                _logger.ErrorException("Error recording to {0}", ex, recordPath);
 | 
				
			||||||
                recordingStatus = RecordingStatus.Error;
 | 
					                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)
 | 
					            if (recordingStatus == RecordingStatus.Completed)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                timer.Status = RecordingStatus.Completed;
 | 
					                timer.Status = RecordingStatus.Completed;
 | 
				
			||||||
 | 
				
			|||||||
@ -68,18 +68,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        public async Task Record(MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
 | 
					        public async Task Record(MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            if (mediaSource.Path.IndexOf("m3u8", StringComparison.OrdinalIgnoreCase) != -1)
 | 
					            var durationToken = new CancellationTokenSource(duration);
 | 
				
			||||||
            {
 | 
					            cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token;
 | 
				
			||||||
                await RecordWithoutTempFile(mediaSource, targetFile, duration, onStarted, cancellationToken)
 | 
					 | 
				
			||||||
                        .ConfigureAwait(false);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                return;
 | 
					            await RecordFromFile(mediaSource, mediaSource.Path, targetFile, false, duration, onStarted, cancellationToken).ConfigureAwait(false);
 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            var tempfile = Path.Combine(_appPaths.TranscodingTempPath, Guid.NewGuid().ToString("N") + ".ts");
 | 
					            _logger.Info("Recording completed to file {0}", targetFile);
 | 
				
			||||||
 | 
					 | 
				
			||||||
            await RecordWithTempFile(mediaSource, tempfile, targetFile, duration, onStarted, cancellationToken)
 | 
					 | 
				
			||||||
                    .ConfigureAwait(false);
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private async void DeleteTempFile(string path)
 | 
					        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)
 | 
					        private Task RecordFromFile(MediaSourceInfo mediaSource, string inputFile, string targetFile, bool deleteInputFileAfterCompletion, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            _targetPath = targetFile;
 | 
					            _targetPath = targetFile;
 | 
				
			||||||
 | 
				
			|||||||
@ -10,6 +10,7 @@ using System.IO;
 | 
				
			|||||||
using System.Linq;
 | 
					using System.Linq;
 | 
				
			||||||
using System.Threading;
 | 
					using System.Threading;
 | 
				
			||||||
using System.Threading.Tasks;
 | 
					using System.Threading.Tasks;
 | 
				
			||||||
 | 
					using MediaBrowser.Controller.Configuration;
 | 
				
			||||||
using MediaBrowser.Controller.MediaEncoding;
 | 
					using MediaBrowser.Controller.MediaEncoding;
 | 
				
			||||||
using MediaBrowser.Model.Dlna;
 | 
					using MediaBrowser.Model.Dlna;
 | 
				
			||||||
using MediaBrowser.Model.Serialization;
 | 
					using MediaBrowser.Model.Serialization;
 | 
				
			||||||
@ -18,7 +19,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
    public abstract class BaseTunerHost
 | 
					    public abstract class BaseTunerHost
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        protected readonly IConfigurationManager Config;
 | 
					        protected readonly IServerConfigurationManager Config;
 | 
				
			||||||
        protected readonly ILogger Logger;
 | 
					        protected readonly ILogger Logger;
 | 
				
			||||||
        protected IJsonSerializer JsonSerializer;
 | 
					        protected IJsonSerializer JsonSerializer;
 | 
				
			||||||
        protected readonly IMediaEncoder MediaEncoder;
 | 
					        protected readonly IMediaEncoder MediaEncoder;
 | 
				
			||||||
@ -26,7 +27,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
 | 
				
			|||||||
        private readonly ConcurrentDictionary<string, ChannelCache> _channelCache =
 | 
					        private readonly ConcurrentDictionary<string, ChannelCache> _channelCache =
 | 
				
			||||||
            new ConcurrentDictionary<string, ChannelCache>(StringComparer.OrdinalIgnoreCase);
 | 
					            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;
 | 
					            Config = config;
 | 
				
			||||||
            Logger = logger;
 | 
					            Logger = logger;
 | 
				
			||||||
@ -125,12 +126,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
                foreach (var host in hostsWithChannel)
 | 
					                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
 | 
					                    try
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                        // Check to make sure the tuner is available
 | 
					                        // Check to make sure the tuner is available
 | 
				
			||||||
@ -156,93 +151,63 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
 | 
				
			|||||||
                    {
 | 
					                    {
 | 
				
			||||||
                        Logger.Error("Error opening tuner", ex);
 | 
					                        Logger.Error("Error opening tuner", ex);
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    finally
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        resourcePool.Release();
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return new List<MediaSourceInfo>();
 | 
					            return new List<MediaSourceInfo>();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        protected abstract Task<MediaSourceInfo> GetChannelStream(TunerHostInfo tuner, string channelId, string streamId, CancellationToken cancellationToken);
 | 
					        protected abstract Task<LiveStream> GetChannelStream(TunerHostInfo tuner, string channelId, string streamId, CancellationToken cancellationToken);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public async Task<Tuple<MediaSourceInfo, SemaphoreSlim>> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken)
 | 
					        public async Task<LiveStream> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            if (IsValidChannelId(channelId))
 | 
					            if (!IsValidChannelId(channelId))
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                var hosts = GetTunerHosts();
 | 
					                throw new FileNotFoundException();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                var hostsWithChannel = new List<TunerHostInfo>();
 | 
					            var hosts = GetTunerHosts();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                foreach (var host in hosts)
 | 
					            var hostsWithChannel = new List<TunerHostInfo>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            foreach (var host in hosts)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                if (string.IsNullOrWhiteSpace(streamId))
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                    if (string.IsNullOrWhiteSpace(streamId))
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        try
 | 
					 | 
				
			||||||
                        {
 | 
					 | 
				
			||||||
                            var channels = await GetChannels(host, true, cancellationToken).ConfigureAwait(false);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            if (channels.Any(i => string.Equals(i.Id, channelId, StringComparison.OrdinalIgnoreCase)))
 | 
					 | 
				
			||||||
                            {
 | 
					 | 
				
			||||||
                                hostsWithChannel.Add(host);
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                        catch (Exception ex)
 | 
					 | 
				
			||||||
                        {
 | 
					 | 
				
			||||||
                            Logger.Error("Error getting channels", ex);
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    else if (streamId.StartsWith(host.Id, StringComparison.OrdinalIgnoreCase))
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        hostsWithChannel = new List<TunerHostInfo> {host};
 | 
					 | 
				
			||||||
                        streamId = streamId.Substring(host.Id.Length);
 | 
					 | 
				
			||||||
                        break;
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                foreach (var host in hostsWithChannel)
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    var resourcePool = GetLock(host.Url);
 | 
					 | 
				
			||||||
                    Logger.Debug("GetChannelStream - Waiting on tuner resource pool");
 | 
					 | 
				
			||||||
                    await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
 | 
					 | 
				
			||||||
                    Logger.Debug("GetChannelStream - Unlocked resource pool");
 | 
					 | 
				
			||||||
                    try
 | 
					                    try
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                        // Check to make sure the tuner is available
 | 
					                        var channels = await GetChannels(host, true, cancellationToken).ConfigureAwait(false);
 | 
				
			||||||
                        // 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 (channels.Any(i => string.Equals(i.Id, channelId, StringComparison.OrdinalIgnoreCase)))
 | 
				
			||||||
                        if (string.IsNullOrWhiteSpace(streamId) && hostsWithChannel.Count > 1)
 | 
					 | 
				
			||||||
                        {
 | 
					                        {
 | 
				
			||||||
                            if (!await IsAvailable(host, channelId, cancellationToken).ConfigureAwait(false))
 | 
					                            hostsWithChannel.Add(host);
 | 
				
			||||||
                            {
 | 
					 | 
				
			||||||
                                Logger.Error("Tuner is not currently available");
 | 
					 | 
				
			||||||
                                resourcePool.Release();
 | 
					 | 
				
			||||||
                                continue;
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
 | 
					 | 
				
			||||||
                        var stream = await GetChannelStream(host, channelId, streamId, cancellationToken).ConfigureAwait(false);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        if (EnableMediaProbing)
 | 
					 | 
				
			||||||
                        {
 | 
					 | 
				
			||||||
                            await AddMediaInfo(stream, false, resourcePool, cancellationToken).ConfigureAwait(false);
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        return new Tuple<MediaSourceInfo, SemaphoreSlim>(stream, resourcePool);
 | 
					 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    catch (Exception ex)
 | 
					                    catch (Exception ex)
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                        Logger.Error("Error opening tuner", ex);
 | 
					                        Logger.Error("Error getting channels", ex);
 | 
				
			||||||
 | 
					 | 
				
			||||||
                        resourcePool.Release();
 | 
					 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					                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();
 | 
					            throw new LiveTvConflictException();
 | 
				
			||||||
@ -268,37 +233,23 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        protected abstract Task<bool> IsAvailableInternal(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken);
 | 
					        protected abstract Task<bool> IsAvailableInternal(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        private async Task AddMediaInfo(LiveStream stream, bool isAudio, CancellationToken cancellationToken)
 | 
				
			||||||
        /// 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));
 | 
					            //await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private async Task AddMediaInfo(MediaSourceInfo mediaSource, bool isAudio, SemaphoreSlim resourcePool, CancellationToken cancellationToken)
 | 
					            //try
 | 
				
			||||||
        {
 | 
					            //{
 | 
				
			||||||
            await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
 | 
					            //    await AddMediaInfoInternal(mediaSource, isAudio, cancellationToken).ConfigureAwait(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            try
 | 
					            //    // Leave the resource locked. it will be released upstream
 | 
				
			||||||
            {
 | 
					            //}
 | 
				
			||||||
                await AddMediaInfoInternal(mediaSource, isAudio, cancellationToken).ConfigureAwait(false);
 | 
					            //catch (Exception)
 | 
				
			||||||
 | 
					            //{
 | 
				
			||||||
 | 
					            //    // Release the resource if there's some kind of failure.
 | 
				
			||||||
 | 
					            //    resourcePool.Release();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                // Leave the resource locked. it will be released upstream
 | 
					            //    throw;
 | 
				
			||||||
            }
 | 
					            //}
 | 
				
			||||||
            catch (Exception)
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                // Release the resource if there's some kind of failure.
 | 
					 | 
				
			||||||
                resourcePool.Release();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                throw;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private async Task AddMediaInfoInternal(MediaSourceInfo mediaSource, bool isAudio, CancellationToken cancellationToken)
 | 
					        private async Task AddMediaInfoInternal(MediaSourceInfo mediaSource, bool isAudio, CancellationToken cancellationToken)
 | 
				
			||||||
 | 
				
			|||||||
@ -14,7 +14,10 @@ using System.IO;
 | 
				
			|||||||
using System.Linq;
 | 
					using System.Linq;
 | 
				
			||||||
using System.Threading;
 | 
					using System.Threading;
 | 
				
			||||||
using System.Threading.Tasks;
 | 
					using System.Threading.Tasks;
 | 
				
			||||||
 | 
					using CommonIO;
 | 
				
			||||||
using MediaBrowser.Common.Extensions;
 | 
					using MediaBrowser.Common.Extensions;
 | 
				
			||||||
 | 
					using MediaBrowser.Controller;
 | 
				
			||||||
 | 
					using MediaBrowser.Controller.Configuration;
 | 
				
			||||||
using MediaBrowser.Controller.MediaEncoding;
 | 
					using MediaBrowser.Controller.MediaEncoding;
 | 
				
			||||||
using MediaBrowser.Model.Configuration;
 | 
					using MediaBrowser.Model.Configuration;
 | 
				
			||||||
using MediaBrowser.Model.Net;
 | 
					using MediaBrowser.Model.Net;
 | 
				
			||||||
@ -24,11 +27,15 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
 | 
				
			|||||||
    public class HdHomerunHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost
 | 
					    public class HdHomerunHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        private readonly IHttpClient _httpClient;
 | 
					        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)
 | 
					            : base(config, logger, jsonSerializer, mediaEncoder)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            _httpClient = httpClient;
 | 
					            _httpClient = httpClient;
 | 
				
			||||||
 | 
					            _fileSystem = fileSystem;
 | 
				
			||||||
 | 
					            _appHost = appHost;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public string Name
 | 
					        public string Name
 | 
				
			||||||
@ -355,6 +362,13 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
 | 
				
			|||||||
                url += "?transcode=" + profile;
 | 
					                url += "?transcode=" + profile;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            var id = profile;
 | 
				
			||||||
 | 
					            if (string.IsNullOrWhiteSpace(id))
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                id = "native";
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            id += "_" + url.GetMD5().ToString("N");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            var mediaSource = new MediaSourceInfo
 | 
					            var mediaSource = new MediaSourceInfo
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                Path = url,
 | 
					                Path = url,
 | 
				
			||||||
@ -387,9 +401,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
 | 
				
			|||||||
                RequiresClosing = false,
 | 
					                RequiresClosing = false,
 | 
				
			||||||
                BufferMs = 0,
 | 
					                BufferMs = 0,
 | 
				
			||||||
                Container = "ts",
 | 
					                Container = "ts",
 | 
				
			||||||
                Id = profile,
 | 
					                Id = id,
 | 
				
			||||||
                SupportsDirectPlay = true,
 | 
					                SupportsDirectPlay = false,
 | 
				
			||||||
                SupportsDirectStream = false,
 | 
					                SupportsDirectStream = true,
 | 
				
			||||||
                SupportsTranscoding = true
 | 
					                SupportsTranscoding = true
 | 
				
			||||||
            };
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -452,9 +466,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
 | 
				
			|||||||
            return channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase);
 | 
					            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))
 | 
					            if (!channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase))
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
@ -462,7 +478,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
            var hdhrId = GetHdHrIdFromChannelId(channelId);
 | 
					            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)
 | 
					        public async Task Validate(TunerHostInfo info)
 | 
				
			||||||
 | 
				
			|||||||
@ -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);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -13,8 +13,10 @@ using System.Threading;
 | 
				
			|||||||
using System.Threading.Tasks;
 | 
					using System.Threading.Tasks;
 | 
				
			||||||
using CommonIO;
 | 
					using CommonIO;
 | 
				
			||||||
using MediaBrowser.Common.Net;
 | 
					using MediaBrowser.Common.Net;
 | 
				
			||||||
 | 
					using MediaBrowser.Controller.Configuration;
 | 
				
			||||||
using MediaBrowser.Controller.MediaEncoding;
 | 
					using MediaBrowser.Controller.MediaEncoding;
 | 
				
			||||||
using MediaBrowser.Model.Serialization;
 | 
					using MediaBrowser.Model.Serialization;
 | 
				
			||||||
 | 
					using MediaBrowser.Server.Implementations.LiveTv.EmbyTV;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
 | 
					namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
@ -23,7 +25,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
 | 
				
			|||||||
        private readonly IFileSystem _fileSystem;
 | 
					        private readonly IFileSystem _fileSystem;
 | 
				
			||||||
        private readonly IHttpClient _httpClient;
 | 
					        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)
 | 
					            : base(config, logger, jsonSerializer, mediaEncoder)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            _fileSystem = fileSystem;
 | 
					            _fileSystem = fileSystem;
 | 
				
			||||||
@ -63,11 +65,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
 | 
				
			|||||||
            return Task.FromResult(list);
 | 
					            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);
 | 
					            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)
 | 
					        public async Task Validate(TunerHostInfo info)
 | 
				
			||||||
@ -136,7 +139,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
 | 
				
			|||||||
                    RequiresOpening = false,
 | 
					                    RequiresOpening = false,
 | 
				
			||||||
                    RequiresClosing = false,
 | 
					                    RequiresClosing = false,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    ReadAtNativeFramerate = false
 | 
					                    ReadAtNativeFramerate = false,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    Id = channel.Path.GetMD5().ToString("N")
 | 
				
			||||||
                };
 | 
					                };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                return new List<MediaSourceInfo> { mediaSource };
 | 
					                return new List<MediaSourceInfo> { mediaSource };
 | 
				
			||||||
 | 
				
			|||||||
@ -8,6 +8,7 @@ using CommonIO;
 | 
				
			|||||||
using MediaBrowser.Common.Configuration;
 | 
					using MediaBrowser.Common.Configuration;
 | 
				
			||||||
using MediaBrowser.Common.Extensions;
 | 
					using MediaBrowser.Common.Extensions;
 | 
				
			||||||
using MediaBrowser.Common.Net;
 | 
					using MediaBrowser.Common.Net;
 | 
				
			||||||
 | 
					using MediaBrowser.Controller.Configuration;
 | 
				
			||||||
using MediaBrowser.Controller.LiveTv;
 | 
					using MediaBrowser.Controller.LiveTv;
 | 
				
			||||||
using MediaBrowser.Controller.MediaEncoding;
 | 
					using MediaBrowser.Controller.MediaEncoding;
 | 
				
			||||||
using MediaBrowser.Model.Dto;
 | 
					using MediaBrowser.Model.Dto;
 | 
				
			||||||
@ -16,6 +17,7 @@ using MediaBrowser.Model.LiveTv;
 | 
				
			|||||||
using MediaBrowser.Model.Logging;
 | 
					using MediaBrowser.Model.Logging;
 | 
				
			||||||
using MediaBrowser.Model.MediaInfo;
 | 
					using MediaBrowser.Model.MediaInfo;
 | 
				
			||||||
using MediaBrowser.Model.Serialization;
 | 
					using MediaBrowser.Model.Serialization;
 | 
				
			||||||
 | 
					using MediaBrowser.Server.Implementations.LiveTv.EmbyTV;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp
 | 
					namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
@ -24,7 +26,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp
 | 
				
			|||||||
        private readonly IFileSystem _fileSystem;
 | 
					        private readonly IFileSystem _fileSystem;
 | 
				
			||||||
        private readonly IHttpClient _httpClient;
 | 
					        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)
 | 
					            : base(config, logger, jsonSerializer, mediaEncoder)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            _fileSystem = fileSystem;
 | 
					            _fileSystem = fileSystem;
 | 
				
			||||||
@ -113,11 +115,13 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp
 | 
				
			|||||||
            return new List<MediaSourceInfo>();
 | 
					            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);
 | 
					            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)
 | 
					        protected override async Task<bool> IsAvailableInternal(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken)
 | 
				
			||||||
 | 
				
			|||||||
@ -241,6 +241,7 @@
 | 
				
			|||||||
    <Compile Include="LiveTv\TunerHosts\BaseTunerHost.cs" />
 | 
					    <Compile Include="LiveTv\TunerHosts\BaseTunerHost.cs" />
 | 
				
			||||||
    <Compile Include="LiveTv\TunerHosts\HdHomerun\HdHomerunHost.cs" />
 | 
					    <Compile Include="LiveTv\TunerHosts\HdHomerun\HdHomerunHost.cs" />
 | 
				
			||||||
    <Compile Include="LiveTv\TunerHosts\HdHomerun\HdHomerunDiscovery.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\M3uParser.cs" />
 | 
				
			||||||
    <Compile Include="LiveTv\TunerHosts\M3UTunerHost.cs" />
 | 
					    <Compile Include="LiveTv\TunerHosts\M3UTunerHost.cs" />
 | 
				
			||||||
    <Compile Include="LiveTv\ProgramImageProvider.cs" />
 | 
					    <Compile Include="LiveTv\ProgramImageProvider.cs" />
 | 
				
			||||||
 | 
				
			|||||||
@ -104,6 +104,12 @@
 | 
				
			|||||||
    <Content Include="dashboard-ui\camerauploadsettings.html">
 | 
					    <Content Include="dashboard-ui\camerauploadsettings.html">
 | 
				
			||||||
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
 | 
					      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
 | 
				
			||||||
    </Content>
 | 
					    </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">
 | 
					    <Content Include="dashboard-ui\components\appfooter\appfooter.css">
 | 
				
			||||||
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
 | 
					      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
 | 
				
			||||||
    </Content>
 | 
					    </Content>
 | 
				
			||||||
@ -437,15 +443,6 @@
 | 
				
			|||||||
    <Content Include="dashboard-ui\scripts\sections.js">
 | 
					    <Content Include="dashboard-ui\scripts\sections.js">
 | 
				
			||||||
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
 | 
					      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
 | 
				
			||||||
    </Content>
 | 
					    </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">
 | 
					    <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\jqm.listview.css">
 | 
				
			||||||
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
 | 
					      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
 | 
				
			||||||
    </Content>
 | 
					    </Content>
 | 
				
			||||||
@ -470,9 +467,6 @@
 | 
				
			|||||||
    <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\jqm.widget.js">
 | 
					    <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\jqm.widget.js">
 | 
				
			||||||
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
 | 
					      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
 | 
				
			||||||
    </Content>
 | 
					    </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">
 | 
					    <Content Include="dashboard-ui\thirdparty\paper-button-style.css">
 | 
				
			||||||
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
 | 
					      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
 | 
				
			||||||
    </Content>
 | 
					    </Content>
 | 
				
			||||||
 | 
				
			|||||||
@ -91,6 +91,16 @@ namespace MediaBrowser.XbmcMetadata
 | 
				
			|||||||
                return;
 | 
					                return;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (!item.SupportsLocalMetadata)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (!item.IsSaveLocalMetadataEnabled())
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            try
 | 
					            try
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                await _providerManager.SaveMetadata(item, updateReason, new[] { BaseNfoSaver.SaverName }).ConfigureAwait(false);
 | 
					                await _providerManager.SaveMetadata(item, updateReason, new[] { BaseNfoSaver.SaverName }).ConfigureAwait(false);
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user