diff --git a/MediaBrowser.Api/ApiEntryPoint.cs b/MediaBrowser.Api/ApiEntryPoint.cs index 785cc395c..d37e7f724 100644 --- a/MediaBrowser.Api/ApiEntryPoint.cs +++ b/MediaBrowser.Api/ApiEntryPoint.cs @@ -349,26 +349,42 @@ namespace MediaBrowser.Api // Also don't cache video if (!hasExitedSuccessfully || job.StartTimeTicks.HasValue || job.IsVideo) { - Logger.Info("Deleting partial stream file(s) {0}", job.Path); + DeletePartialStreamFiles(job.Path, job.Type, 0, 1500); + } + } - await Task.Delay(1500).ConfigureAwait(false); + private async void DeletePartialStreamFiles(string path, TranscodingJobType jobType, int retryCount, int delayMs) + { + if (retryCount >= 5) + { + return; + } - try + Logger.Info("Deleting partial stream file(s) {0}", path); + + await Task.Delay(delayMs).ConfigureAwait(false); + + try + { + if (jobType == TranscodingJobType.Progressive) { - if (job.Type == TranscodingJobType.Progressive) - { - DeleteProgressivePartialStreamFiles(job.Path); - } - else - { - DeleteHlsPartialStreamFiles(job.Path); - } + DeleteProgressivePartialStreamFiles(path); } - catch (IOException ex) + else { - Logger.ErrorException("Error deleting partial stream file(s) {0}", ex, job.Path); + DeleteHlsPartialStreamFiles(path); } } + catch (IOException ex) + { + Logger.ErrorException("Error deleting partial stream file(s) {0}", ex, path); + + DeletePartialStreamFiles(path, jobType, retryCount + 1, 500); + } + catch (Exception ex) + { + Logger.ErrorException("Error deleting partial stream file(s) {0}", ex, path); + } } /// diff --git a/MediaBrowser.Api/MediaBrowser.Api.csproj b/MediaBrowser.Api/MediaBrowser.Api.csproj index 0732ee00c..4fc989fc5 100644 --- a/MediaBrowser.Api/MediaBrowser.Api.csproj +++ b/MediaBrowser.Api/MediaBrowser.Api.csproj @@ -97,6 +97,7 @@ + diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index 450c0c681..1ce7d2db3 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -13,6 +13,7 @@ using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; +using MediaBrowser.Model.LiveTv; using System; using System.Collections.Generic; using System.Diagnostics; @@ -539,8 +540,8 @@ namespace MediaBrowser.Api.Playback /// System.String. protected string GetProbeSizeArgument(string mediaPath, bool isVideo, VideoType? videoType, IsoType? isoType) { - var type = !isVideo ? MediaEncoderHelpers.GetInputType(mediaPath, null, null) : - MediaEncoderHelpers.GetInputType(mediaPath, videoType, isoType); + var type = !isVideo ? MediaEncoderHelpers.GetInputType(null, null) : + MediaEncoderHelpers.GetInputType(videoType, isoType); return MediaEncoder.GetProbeSizeArgument(type); } @@ -654,6 +655,11 @@ namespace MediaBrowser.Api.Playback /// System.String. protected string GetInputArgument(StreamState state) { + if (state.SendInputOverStandardInput) + { + return "-"; + } + var type = InputType.AudioFile; var inputPath = new[] { state.MediaPath }; @@ -705,7 +711,9 @@ namespace MediaBrowser.Api.Playback Arguments = GetCommandLineArguments(outputPath, state, true), WindowStyle = ProcessWindowStyle.Hidden, - ErrorDialog = false + ErrorDialog = false, + + RedirectStandardInput = state.SendInputOverStandardInput }, EnableRaisingEvents = true @@ -738,6 +746,11 @@ namespace MediaBrowser.Api.Playback throw; } + if (state.SendInputOverStandardInput) + { + StreamToStandardInput(process, state); + } + // MUST read both stdout and stderr asynchronously or a deadlock may occurr process.BeginOutputReadLine(); @@ -763,6 +776,34 @@ namespace MediaBrowser.Api.Playback } } + private async void StreamToStandardInput(Process process, StreamState state) + { + state.StandardInputCancellationTokenSource = new CancellationTokenSource(); + + try + { + await StreamToStandardInputInternal(process, state).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + Logger.Debug("Stream to standard input closed normally."); + } + catch (Exception ex) + { + Logger.ErrorException("Error writing to standard input", ex); + } + } + + private async Task StreamToStandardInputInternal(Process process, StreamState state) + { + state.StandardInputCancellationTokenSource = new CancellationTokenSource(); + + using (var fileStream = FileSystem.GetFileStream(state.MediaPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true)) + { + await new EndlessStreamCopy().CopyStream(fileStream, process.StandardInput.BaseStream, state.StandardInputCancellationTokenSource.Token).ConfigureAwait(false); + } + } + protected int? GetVideoBitrateParam(StreamState state) { return state.VideoRequest.VideoBitRate; @@ -831,6 +872,11 @@ namespace MediaBrowser.Api.Playback state.IsoMount = null; } + if (state.StandardInputCancellationTokenSource != null) + { + state.StandardInputCancellationTokenSource.Cancel(); + } + var outputFilePath = GetOutputFilePath(state); state.LogFileStream.Dispose(); @@ -903,10 +949,11 @@ namespace MediaBrowser.Api.Playback } itemId = recording.Id; + state.SendInputOverStandardInput = recording.RecordingInfo.Status == RecordingStatus.InProgress; } else if (string.Equals(request.Type, "Channel", StringComparison.OrdinalIgnoreCase)) { - var channel = LiveTvManager.GetInternalChannel(request.Id); + var channel = LiveTvManager.GetInternalChannel(request.Id); state.VideoType = VideoType.VideoFile; state.IsInputVideo = string.Equals(channel.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase); @@ -926,6 +973,7 @@ namespace MediaBrowser.Api.Playback } itemId = channel.Id; + state.SendInputOverStandardInput = true; } else { diff --git a/MediaBrowser.Api/Playback/EndlessStreamCopy.cs b/MediaBrowser.Api/Playback/EndlessStreamCopy.cs new file mode 100644 index 000000000..40586261f --- /dev/null +++ b/MediaBrowser.Api/Playback/EndlessStreamCopy.cs @@ -0,0 +1,32 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Api.Playback +{ + public class EndlessStreamCopy + { + public async Task CopyStream(Stream source, Stream target, CancellationToken cancellationToken) + { + long position = 0; + + while (!cancellationToken.IsCancellationRequested) + { + await source.CopyToAsync(target, 81920, cancellationToken).ConfigureAwait(false); + + var fsPosition = source.Position; + + var bytesRead = fsPosition - position; + + //Logger.Debug("Streamed {0} bytes from file {1}", bytesRead, path); + + if (bytesRead == 0) + { + await Task.Delay(100, cancellationToken).ConfigureAwait(false); + } + + position = fsPosition; + } + } + } +} diff --git a/MediaBrowser.Api/Playback/StreamState.cs b/MediaBrowser.Api/Playback/StreamState.cs index be1ad85eb..f705b5e33 100644 --- a/MediaBrowser.Api/Playback/StreamState.cs +++ b/MediaBrowser.Api/Playback/StreamState.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Model.Entities; +using System.Threading; +using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using System.Collections.Generic; using System.IO; @@ -47,5 +48,9 @@ namespace MediaBrowser.Api.Playback public List PlayableStreamFileNames { get; set; } public bool HasMediaStreams { get; set; } + + public bool SendInputOverStandardInput { get; set; } + + public CancellationTokenSource StandardInputCancellationTokenSource { get; set; } } } diff --git a/MediaBrowser.Controller/MediaInfo/MediaEncoderHelpers.cs b/MediaBrowser.Controller/MediaInfo/MediaEncoderHelpers.cs index 904ecdf93..6b8276eb1 100644 --- a/MediaBrowser.Controller/MediaInfo/MediaEncoderHelpers.cs +++ b/MediaBrowser.Controller/MediaInfo/MediaEncoderHelpers.cs @@ -77,15 +77,14 @@ namespace MediaBrowser.Controller.MediaInfo .Where(f => !string.IsNullOrEmpty(f)) .ToList(); } - + /// /// Gets the type of the input. /// - /// The path. /// Type of the video. /// Type of the iso. /// InputType. - public static InputType GetInputType(string path, VideoType? videoType, IsoType? isoType) + public static InputType GetInputType(VideoType? videoType, IsoType? isoType) { var type = InputType.AudioFile; diff --git a/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs b/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs index 4b3f33297..0b1cfe35c 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs @@ -79,8 +79,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv if (user != null) { + // Avoid implicitly captured closure + var currentUser = user; + channels = channels - .Where(i => i.IsParentalAllowed(user)) + .Where(i => i.IsParentalAllowed(currentUser)) .OrderBy(i => { double number = 0; @@ -419,7 +422,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv var allChannelsList = allChannels.ToList(); var list = new List(); - var programs = new List(); var numComplete = 0; @@ -429,13 +431,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv { var item = await GetChannel(channelInfo.Item2, channelInfo.Item1, cancellationToken).ConfigureAwait(false); - var channelPrograms = await service.GetProgramsAsync(channelInfo.Item2.Id, cancellationToken).ConfigureAwait(false); - - var programTasks = channelPrograms.Select(program => GetProgram(program, item.ChannelInfo.ChannelType, service.Name, cancellationToken)); - var programEntities = await Task.WhenAll(programTasks).ConfigureAwait(false); - - programs.AddRange(programEntities); - list.Add(item); } catch (OperationCanceledException) @@ -451,11 +446,45 @@ namespace MediaBrowser.Server.Implementations.LiveTv double percent = numComplete; percent /= allChannelsList.Count; + progress.Report(5 * percent + 10); + } + _channels = list.ToDictionary(i => i.Id); + progress.Report(15); + + numComplete = 0; + var programs = new List(); + + foreach (var item in list) + { + // Avoid implicitly captured closure + var currentChannel = item; + + try + { + var channelPrograms = await service.GetProgramsAsync(currentChannel.ChannelInfo.Id, cancellationToken).ConfigureAwait(false); + + var programTasks = channelPrograms.Select(program => GetProgram(program, currentChannel.ChannelInfo.ChannelType, service.Name, cancellationToken)); + var programEntities = await Task.WhenAll(programTasks).ConfigureAwait(false); + + programs.AddRange(programEntities); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + _logger.ErrorException("Error getting programs for channel {0}", ex, currentChannel.Name); + } + + numComplete++; + double percent = numComplete; + percent /= allChannelsList.Count; + progress.Report(90 * percent + 10); } _programs = programs.ToDictionary(i => i.Id); - _channels = list.ToDictionary(i => i.Id); } private async Task>> GetChannels(ILiveTvService service, CancellationToken cancellationToken) diff --git a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj index d544d4f30..4aa3861f1 100644 --- a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj +++ b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj @@ -127,6 +127,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest @@ -136,6 +139,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest