Browse Source

added live channel playback

Luke Pulverenti 11 years ago
parent
commit
ede84702d1

+ 29 - 13
MediaBrowser.Api/ApiEntryPoint.cs

@@ -349,26 +349,42 @@ namespace MediaBrowser.Api
             // Also don't cache video
             // Also don't cache video
             if (!hasExitedSuccessfully || job.StartTimeTicks.HasValue || job.IsVideo)
             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);
+            }
         }
         }
 
 
         /// <summary>
         /// <summary>

+ 1 - 0
MediaBrowser.Api/MediaBrowser.Api.csproj

@@ -97,6 +97,7 @@
     <Compile Include="NotificationsService.cs" />
     <Compile Include="NotificationsService.cs" />
     <Compile Include="PackageReviewService.cs" />
     <Compile Include="PackageReviewService.cs" />
     <Compile Include="PackageService.cs" />
     <Compile Include="PackageService.cs" />
+    <Compile Include="Playback\EndlessStreamCopy.cs" />
     <Compile Include="Playback\Hls\AudioHlsService.cs" />
     <Compile Include="Playback\Hls\AudioHlsService.cs" />
     <Compile Include="Playback\Hls\BaseHlsService.cs" />
     <Compile Include="Playback\Hls\BaseHlsService.cs" />
     <Compile Include="Playback\Hls\HlsSegmentResponseFilter.cs" />
     <Compile Include="Playback\Hls\HlsSegmentResponseFilter.cs" />

+ 52 - 4
MediaBrowser.Api/Playback/BaseStreamingService.cs

@@ -13,6 +13,7 @@ using MediaBrowser.Model.Drawing;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
+using MediaBrowser.Model.LiveTv;
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.Diagnostics;
@@ -539,8 +540,8 @@ namespace MediaBrowser.Api.Playback
         /// <returns>System.String.</returns>
         /// <returns>System.String.</returns>
         protected string GetProbeSizeArgument(string mediaPath, bool isVideo, VideoType? videoType, IsoType? isoType)
         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);
             return MediaEncoder.GetProbeSizeArgument(type);
         }
         }
@@ -654,6 +655,11 @@ namespace MediaBrowser.Api.Playback
         /// <returns>System.String.</returns>
         /// <returns>System.String.</returns>
         protected string GetInputArgument(StreamState state)
         protected string GetInputArgument(StreamState state)
         {
         {
+            if (state.SendInputOverStandardInput)
+            {
+                return "-";
+            }
+
             var type = InputType.AudioFile;
             var type = InputType.AudioFile;
 
 
             var inputPath = new[] { state.MediaPath };
             var inputPath = new[] { state.MediaPath };
@@ -705,7 +711,9 @@ namespace MediaBrowser.Api.Playback
                     Arguments = GetCommandLineArguments(outputPath, state, true),
                     Arguments = GetCommandLineArguments(outputPath, state, true),
 
 
                     WindowStyle = ProcessWindowStyle.Hidden,
                     WindowStyle = ProcessWindowStyle.Hidden,
-                    ErrorDialog = false
+                    ErrorDialog = false,
+
+                    RedirectStandardInput = state.SendInputOverStandardInput
                 },
                 },
 
 
                 EnableRaisingEvents = true
                 EnableRaisingEvents = true
@@ -738,6 +746,11 @@ namespace MediaBrowser.Api.Playback
                 throw;
                 throw;
             }
             }
 
 
+            if (state.SendInputOverStandardInput)
+            {
+                StreamToStandardInput(process, state);
+            }
+
             // MUST read both stdout and stderr asynchronously or a deadlock may occurr
             // MUST read both stdout and stderr asynchronously or a deadlock may occurr
             process.BeginOutputReadLine();
             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)
         protected int? GetVideoBitrateParam(StreamState state)
         {
         {
             return state.VideoRequest.VideoBitRate;
             return state.VideoRequest.VideoBitRate;
@@ -831,6 +872,11 @@ namespace MediaBrowser.Api.Playback
                 state.IsoMount = null;
                 state.IsoMount = null;
             }
             }
 
 
+            if (state.StandardInputCancellationTokenSource != null)
+            {
+                state.StandardInputCancellationTokenSource.Cancel();
+            }
+
             var outputFilePath = GetOutputFilePath(state);
             var outputFilePath = GetOutputFilePath(state);
 
 
             state.LogFileStream.Dispose();
             state.LogFileStream.Dispose();
@@ -903,10 +949,11 @@ namespace MediaBrowser.Api.Playback
                 }
                 }
 
 
                 itemId = recording.Id;
                 itemId = recording.Id;
+                state.SendInputOverStandardInput = recording.RecordingInfo.Status == RecordingStatus.InProgress;
             }
             }
             else if (string.Equals(request.Type, "Channel", StringComparison.OrdinalIgnoreCase))
             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.VideoType = VideoType.VideoFile;
                 state.IsInputVideo = string.Equals(channel.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase);
                 state.IsInputVideo = string.Equals(channel.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase);
@@ -926,6 +973,7 @@ namespace MediaBrowser.Api.Playback
                 }
                 }
 
 
                 itemId = channel.Id;
                 itemId = channel.Id;
+                state.SendInputOverStandardInput = true;
             }
             }
             else
             else
             {
             {

+ 32 - 0
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;
+            }
+        }
+    }
+}

+ 6 - 1
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 MediaBrowser.Model.IO;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.IO;
 using System.IO;
@@ -47,5 +48,9 @@ namespace MediaBrowser.Api.Playback
         public List<string> PlayableStreamFileNames { get; set; }
         public List<string> PlayableStreamFileNames { get; set; }
 
 
         public bool HasMediaStreams { get; set; }
         public bool HasMediaStreams { get; set; }
+
+        public bool SendInputOverStandardInput { get; set; }
+
+        public CancellationTokenSource StandardInputCancellationTokenSource { get; set; }
     }
     }
 }
 }

+ 2 - 3
MediaBrowser.Controller/MediaInfo/MediaEncoderHelpers.cs

@@ -77,15 +77,14 @@ namespace MediaBrowser.Controller.MediaInfo
                 .Where(f => !string.IsNullOrEmpty(f))
                 .Where(f => !string.IsNullOrEmpty(f))
                 .ToList();
                 .ToList();
         }
         }
-        
+
         /// <summary>
         /// <summary>
         /// Gets the type of the input.
         /// Gets the type of the input.
         /// </summary>
         /// </summary>
-        /// <param name="path">The path.</param>
         /// <param name="videoType">Type of the video.</param>
         /// <param name="videoType">Type of the video.</param>
         /// <param name="isoType">Type of the iso.</param>
         /// <param name="isoType">Type of the iso.</param>
         /// <returns>InputType.</returns>
         /// <returns>InputType.</returns>
-        public static InputType GetInputType(string path, VideoType? videoType, IsoType? isoType)
+        public static InputType GetInputType(VideoType? videoType, IsoType? isoType)
         {
         {
             var type = InputType.AudioFile;
             var type = InputType.AudioFile;
 
 

+ 37 - 8
MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs

@@ -79,8 +79,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv
 
 
             if (user != null)
             if (user != null)
             {
             {
+                // Avoid implicitly captured closure
+                var currentUser = user;
+
                 channels = channels
                 channels = channels
-                    .Where(i => i.IsParentalAllowed(user))
+                    .Where(i => i.IsParentalAllowed(currentUser))
                     .OrderBy(i =>
                     .OrderBy(i =>
                     {
                     {
                         double number = 0;
                         double number = 0;
@@ -419,7 +422,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             var allChannelsList = allChannels.ToList();
             var allChannelsList = allChannels.ToList();
 
 
             var list = new List<LiveTvChannel>();
             var list = new List<LiveTvChannel>();
-            var programs = new List<LiveTvProgram>();
 
 
             var numComplete = 0;
             var numComplete = 0;
 
 
@@ -429,14 +431,42 @@ namespace MediaBrowser.Server.Implementations.LiveTv
                 {
                 {
                     var item = await GetChannel(channelInfo.Item2, channelInfo.Item1, cancellationToken).ConfigureAwait(false);
                     var item = await GetChannel(channelInfo.Item2, channelInfo.Item1, cancellationToken).ConfigureAwait(false);
 
 
-                    var channelPrograms = await service.GetProgramsAsync(channelInfo.Item2.Id, cancellationToken).ConfigureAwait(false);
+                    list.Add(item);
+                }
+                catch (OperationCanceledException)
+                {
+                    throw;
+                }
+                catch (Exception ex)
+                {
+                    _logger.ErrorException("Error getting channel information for {0}", ex, channelInfo.Item2.Name);
+                }
 
 
-                    var programTasks = channelPrograms.Select(program => GetProgram(program, item.ChannelInfo.ChannelType, service.Name, cancellationToken));
+                numComplete++;
+                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<LiveTvProgram>();
+
+            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);
                     var programEntities = await Task.WhenAll(programTasks).ConfigureAwait(false);
 
 
                     programs.AddRange(programEntities);
                     programs.AddRange(programEntities);
-
-                    list.Add(item);
                 }
                 }
                 catch (OperationCanceledException)
                 catch (OperationCanceledException)
                 {
                 {
@@ -444,7 +474,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
                 }
                 }
                 catch (Exception ex)
                 catch (Exception ex)
                 {
                 {
-                    _logger.ErrorException("Error getting channel information for {0}", ex, channelInfo.Item2.Name);
+                    _logger.ErrorException("Error getting programs for channel {0}", ex, currentChannel.Name);
                 }
                 }
 
 
                 numComplete++;
                 numComplete++;
@@ -455,7 +485,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             }
             }
 
 
             _programs = programs.ToDictionary(i => i.Id);
             _programs = programs.ToDictionary(i => i.Id);
-            _channels = list.ToDictionary(i => i.Id);
         }
         }
 
 
         private async Task<IEnumerable<Tuple<string, ChannelInfo>>> GetChannels(ILiveTvService service, CancellationToken cancellationToken)
         private async Task<IEnumerable<Tuple<string, ChannelInfo>>> GetChannels(ILiveTvService service, CancellationToken cancellationToken)

+ 6 - 0
MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj

@@ -127,6 +127,9 @@
     <Content Include="dashboard-ui\css\images\icons\subtitles.png">
     <Content Include="dashboard-ui\css\images\icons\subtitles.png">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
     </Content>
+    <Content Include="dashboard-ui\css\images\icons\tv.png">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
     <Content Include="dashboard-ui\css\images\icons\volumedown.png">
     <Content Include="dashboard-ui\css\images\icons\volumedown.png">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
     </Content>
@@ -136,6 +139,9 @@
     <Content Include="dashboard-ui\css\images\items\detail\tv.png">
     <Content Include="dashboard-ui\css\images\items\detail\tv.png">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
     </Content>
+    <Content Include="dashboard-ui\css\images\media\tvflyout.png">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
     <Content Include="dashboard-ui\css\livetv.css">
     <Content Include="dashboard-ui\css\livetv.css">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
     </Content>