Browse Source

close ffmpeg more gracefully

Luke Pulverenti 11 years ago
parent
commit
4398393783

+ 37 - 39
MediaBrowser.Api/ApiEntryPoint.cs

@@ -3,15 +3,14 @@ using MediaBrowser.Controller;
 using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Session;
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
-using System.ComponentModel;
 using System.Diagnostics;
 using System.Diagnostics;
 using System.IO;
 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.Model.Session;
 
 
 namespace MediaBrowser.Api
 namespace MediaBrowser.Api
 {
 {
@@ -100,7 +99,7 @@ namespace MediaBrowser.Api
         {
         {
             var jobCount = _activeTranscodingJobs.Count;
             var jobCount = _activeTranscodingJobs.Count;
 
 
-            Parallel.ForEach(_activeTranscodingJobs.ToList(), KillTranscodingJob);
+            Parallel.ForEach(_activeTranscodingJobs.ToList(), j => KillTranscodingJob(j, true));
 
 
             // Try to allow for some time to kill the ffmpeg processes and delete the partial stream files
             // Try to allow for some time to kill the ffmpeg processes and delete the partial stream files
             if (jobCount > 0)
             if (jobCount > 0)
@@ -291,16 +290,16 @@ namespace MediaBrowser.Api
         {
         {
             var job = (TranscodingJob)state;
             var job = (TranscodingJob)state;
 
 
-            KillTranscodingJob(job);
+            KillTranscodingJob(job, true);
         }
         }
 
 
         /// <summary>
         /// <summary>
         /// Kills the single transcoding job.
         /// Kills the single transcoding job.
         /// </summary>
         /// </summary>
         /// <param name="deviceId">The device id.</param>
         /// <param name="deviceId">The device id.</param>
-        /// <param name="isVideo">if set to <c>true</c> [is video].</param>
+        /// <param name="deleteFiles">if set to <c>true</c> [delete files].</param>
         /// <exception cref="System.ArgumentNullException">sourcePath</exception>
         /// <exception cref="System.ArgumentNullException">sourcePath</exception>
-        internal void KillTranscodingJobs(string deviceId, bool isVideo)
+        internal void KillTranscodingJobs(string deviceId, bool deleteFiles)
         {
         {
             if (string.IsNullOrEmpty(deviceId))
             if (string.IsNullOrEmpty(deviceId))
             {
             {
@@ -318,7 +317,7 @@ namespace MediaBrowser.Api
 
 
             foreach (var job in jobs)
             foreach (var job in jobs)
             {
             {
-                KillTranscodingJob(job);
+                KillTranscodingJob(job, deleteFiles);
             }
             }
         }
         }
 
 
@@ -326,7 +325,8 @@ namespace MediaBrowser.Api
         /// Kills the transcoding job.
         /// Kills the transcoding job.
         /// </summary>
         /// </summary>
         /// <param name="job">The job.</param>
         /// <param name="job">The job.</param>
-        private void KillTranscodingJob(TranscodingJob job)
+        /// <param name="deleteFiles">if set to <c>true</c> [delete files].</param>
+        private void KillTranscodingJob(TranscodingJob job, bool deleteFiles)
         {
         {
             lock (_activeTranscodingJobs)
             lock (_activeTranscodingJobs)
             {
             {
@@ -344,48 +344,44 @@ namespace MediaBrowser.Api
                 }
                 }
             }
             }
 
 
-            var process = job.Process;
-
-            var hasExited = true;
-
-            try
+            lock (job.ProcessLock)
             {
             {
-                hasExited = process.HasExited;
-            }
-            catch (Exception ex)
-            {
-                Logger.ErrorException("Error determining if ffmpeg process has exited for {0}", ex, job.Path);
-            }
-
-            if (!hasExited)
-            {
-                try
-                {
-                    Logger.Info("Killing ffmpeg process for {0}", job.Path);
+                var process = job.Process;
 
 
-                    process.Kill();
+                var hasExited = true;
 
 
-                    // Need to wait because killing is asynchronous
-                    process.WaitForExit(5000);
-                }
-                catch (Win32Exception ex)
+                try
                 {
                 {
-                    Logger.ErrorException("Error killing transcoding job for {0}", ex, job.Path);
+                    hasExited = process.HasExited;
                 }
                 }
-                catch (InvalidOperationException ex)
+                catch (Exception ex)
                 {
                 {
-                    Logger.ErrorException("Error killing transcoding job for {0}", ex, job.Path);
+                    Logger.ErrorException("Error determining if ffmpeg process has exited for {0}", ex, job.Path);
                 }
                 }
-                catch (NotSupportedException ex)
+
+                if (!hasExited)
                 {
                 {
-                    Logger.ErrorException("Error killing transcoding job for {0}", ex, job.Path);
+                    try
+                    {
+                        Logger.Info("Killing ffmpeg process for {0}", job.Path);
+
+                        //process.Kill();
+                        process.StandardInput.WriteLine("q");
+
+                        // Need to wait because killing is asynchronous
+                        process.WaitForExit(5000);
+                    }
+                    catch (Exception ex)
+                    {
+                        Logger.ErrorException("Error killing transcoding job for {0}", ex, job.Path);
+                    }
                 }
                 }
             }
             }
 
 
-            // Dispose the process
-            process.Dispose();
-
-            DeletePartialStreamFiles(job.Path, job.Type, 0, 1500);
+            if (deleteFiles)
+            {
+                DeletePartialStreamFiles(job.Path, job.Type, 0, 1500);
+            }
         }
         }
 
 
         private async void DeletePartialStreamFiles(string path, TranscodingJobType jobType, int retryCount, int delayMs)
         private async void DeletePartialStreamFiles(string path, TranscodingJobType jobType, int retryCount, int delayMs)
@@ -494,6 +490,8 @@ namespace MediaBrowser.Api
         public string DeviceId { get; set; }
         public string DeviceId { get; set; }
 
 
         public CancellationTokenSource CancellationTokenSource { get; set; }
         public CancellationTokenSource CancellationTokenSource { get; set; }
+
+        public object ProcessLock = new object();
     }
     }
 
 
     /// <summary>
     /// <summary>

+ 15 - 2
MediaBrowser.Api/Playback/BaseStreamingService.cs

@@ -816,6 +816,7 @@ namespace MediaBrowser.Api.Playback
                     // Must consume both stdout and stderr or deadlocks may occur
                     // Must consume both stdout and stderr or deadlocks may occur
                     RedirectStandardOutput = true,
                     RedirectStandardOutput = true,
                     RedirectStandardError = true,
                     RedirectStandardError = true,
+                    RedirectStandardInput = true,
 
 
                     FileName = MediaEncoder.EncoderPath,
                     FileName = MediaEncoder.EncoderPath,
                     WorkingDirectory = Path.GetDirectoryName(MediaEncoder.EncoderPath),
                     WorkingDirectory = Path.GetDirectoryName(MediaEncoder.EncoderPath),
@@ -1073,8 +1074,9 @@ namespace MediaBrowser.Api.Playback
         /// </summary>
         /// </summary>
         /// <param name="process">The process.</param>
         /// <param name="process">The process.</param>
         /// <param name="state">The state.</param>
         /// <param name="state">The state.</param>
-        protected void OnFfMpegProcessExited(Process process, StreamState state)
+        private void OnFfMpegProcessExited(Process process, StreamState state)
         {
         {
+            Logger.Debug("Disposing stream resources");
             state.Dispose();
             state.Dispose();
 
 
             try
             try
@@ -1083,8 +1085,19 @@ namespace MediaBrowser.Api.Playback
             }
             }
             catch
             catch
             {
             {
-                Logger.Info("FFMpeg exited with an error.");
+                Logger.Error("FFMpeg exited with an error.");
             }
             }
+
+            // This causes on exited to be called twice:
+            //try
+            //{
+            //    // Dispose the process
+            //    process.Dispose();
+            //}
+            //catch (Exception ex)
+            //{
+            //    Logger.ErrorException("Error disposing ffmpeg.", ex);
+            //}
         }
         }
 
 
         protected double? GetFramerateParam(StreamState state)
         protected double? GetFramerateParam(StreamState state)

+ 2 - 2
MediaBrowser.Api/Playback/Hls/BaseHlsService.cs

@@ -83,7 +83,7 @@ namespace MediaBrowser.Api.Playback.Hls
         {
         {
             var cancellationTokenSource = new CancellationTokenSource();
             var cancellationTokenSource = new CancellationTokenSource();
 
 
-            var state = GetState(request, cancellationTokenSource.Token).Result;
+            var state = await GetState(request, cancellationTokenSource.Token).ConfigureAwait(false);
 
 
             var playlist = state.OutputFilePath;
             var playlist = state.OutputFilePath;
 
 
@@ -154,7 +154,7 @@ namespace MediaBrowser.Api.Playback.Hls
         /// <returns>System.Int32.</returns>
         /// <returns>System.Int32.</returns>
         protected int GetSegmentWait()
         protected int GetSegmentWait()
         {
         {
-            var minimumSegmentCount = 3;
+            var minimumSegmentCount = 2;
             var quality = GetQualitySetting();
             var quality = GetQualitySetting();
 
 
             if (quality == EncodingQuality.HighSpeed || quality == EncodingQuality.HighQuality)
             if (quality == EncodingQuality.HighSpeed || quality == EncodingQuality.HighQuality)

+ 60 - 16
MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs

@@ -1,13 +1,10 @@
 using MediaBrowser.Common.IO;
 using MediaBrowser.Common.IO;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Dlna;
-using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
 using ServiceStack;
 using ServiceStack;
 using System;
 using System;
@@ -17,7 +14,6 @@ using System.IO;
 using System.Text;
 using System.Text;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
-using MimeTypes = ServiceStack.MimeTypes;
 
 
 namespace MediaBrowser.Api.Playback.Hls
 namespace MediaBrowser.Api.Playback.Hls
 {
 {
@@ -83,29 +79,75 @@ namespace MediaBrowser.Api.Playback.Hls
             return GetDynamicSegment(request, true).Result;
             return GetDynamicSegment(request, true).Result;
         }
         }
 
 
+        private static readonly SemaphoreSlim FfmpegStartLock = new SemaphoreSlim(1, 1);
         private async Task<object> GetDynamicSegment(GetDynamicHlsVideoSegment request, bool isMain)
         private async Task<object> GetDynamicSegment(GetDynamicHlsVideoSegment request, bool isMain)
         {
         {
+            var cancellationTokenSource = new CancellationTokenSource();
+            var cancellationToken = cancellationTokenSource.Token;
+
             var index = int.Parse(request.SegmentId, NumberStyles.Integer, UsCulture);
             var index = int.Parse(request.SegmentId, NumberStyles.Integer, UsCulture);
 
 
-            var state = await GetState(request, CancellationToken.None).ConfigureAwait(false);
+            var state = await GetState(request, cancellationToken).ConfigureAwait(false);
 
 
             var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".m3u8");
             var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".m3u8");
 
 
-            var path = GetSegmentPath(playlistPath, index);
+            var segmentPath = GetSegmentPath(playlistPath, index);
 
 
-            if (File.Exists(path))
+            if (File.Exists(segmentPath))
             {
             {
-                return GetSegementResult(path);
+                ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType.Hls);
+                return GetSegementResult(segmentPath);
             }
             }
 
 
-            if (!File.Exists(playlistPath))
+            await FfmpegStartLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
+            try
+            {
+                if (File.Exists(segmentPath))
+                {
+                    ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType.Hls);
+                    return GetSegementResult(segmentPath);
+                }
+                else
+                {
+                    if (index == 0)
+                    {
+                        // If the playlist doesn't already exist, startup ffmpeg
+                        try
+                        {
+                            ApiEntryPoint.Instance.KillTranscodingJobs(state.Request.DeviceId, false);
+
+                            await StartFfMpeg(state, playlistPath, cancellationTokenSource).ConfigureAwait(false);
+                        }
+                        catch
+                        {
+                            state.Dispose();
+                            throw;
+                        }
+
+                        await WaitForMinimumSegmentCount(playlistPath, 2, cancellationTokenSource.Token).ConfigureAwait(false);
+                    }
+                }
+            }
+            finally
             {
             {
-                await StartFfMpeg(state, playlistPath, new CancellationTokenSource()).ConfigureAwait(false);
+                FfmpegStartLock.Release();
+            }
 
 
-                await WaitForMinimumSegmentCount(playlistPath, GetSegmentWait(), CancellationToken.None).ConfigureAwait(false);
+            Logger.Info("waiting for {0}", segmentPath);
+            while (!File.Exists(segmentPath))
+            {
+                await Task.Delay(50, cancellationToken).ConfigureAwait(false);
             }
             }
 
 
-            return GetSegementResult(path);
+            Logger.Info("returning {0}", segmentPath);
+            return GetSegementResult(segmentPath);
+        }
+
+        protected override int GetStartNumber(StreamState state)
+        {
+            var request = (GetDynamicHlsVideoSegment) state.Request;
+
+            return int.Parse(request.SegmentId, NumberStyles.Integer, UsCulture);
         }
         }
 
 
         private string GetSegmentPath(string playlist, int index)
         private string GetSegmentPath(string playlist, int index)
@@ -120,7 +162,7 @@ namespace MediaBrowser.Api.Playback.Hls
         private object GetSegementResult(string path)
         private object GetSegementResult(string path)
         {
         {
             // TODO: Handle if it's currently being written to
             // TODO: Handle if it's currently being written to
-            return ResultFactory.GetStaticFileResult(Request, path);
+            return ResultFactory.GetStaticFileResult(Request, path, FileShare.ReadWrite);
         }
         }
 
 
         private async Task<object> GetAsync(GetMasterHlsVideoStream request)
         private async Task<object> GetAsync(GetMasterHlsVideoStream request)
@@ -143,7 +185,7 @@ namespace MediaBrowser.Api.Playback.Hls
 
 
             var playlistText = GetMasterPlaylistFileText(videoBitrate + audioBitrate, appendBaselineStream, baselineStreamBitrate);
             var playlistText = GetMasterPlaylistFileText(videoBitrate + audioBitrate, appendBaselineStream, baselineStreamBitrate);
 
 
-            return ResultFactory.GetResult(playlistText, MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
+            return ResultFactory.GetResult(playlistText, Common.Net.MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
         }
         }
 
 
         private string GetMasterPlaylistFileText(int bitrate, bool includeBaselineStream, int baselineStreamBitrate)
         private string GetMasterPlaylistFileText(int bitrate, bool includeBaselineStream, int baselineStreamBitrate)
@@ -226,7 +268,7 @@ namespace MediaBrowser.Api.Playback.Hls
 
 
             var playlistText = builder.ToString();
             var playlistText = builder.ToString();
 
 
-            return ResultFactory.GetResult(playlistText, MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
+            return ResultFactory.GetResult(playlistText, Common.Net.MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
         }
         }
 
 
         protected override string GetAudioArguments(StreamState state)
         protected override string GetAudioArguments(StreamState state)
@@ -274,7 +316,9 @@ namespace MediaBrowser.Api.Playback.Hls
                 return IsH264(state.VideoStream) ? "-codec:v:0 copy -bsf h264_mp4toannexb" : "-codec:v:0 copy";
                 return IsH264(state.VideoStream) ? "-codec:v:0 copy -bsf h264_mp4toannexb" : "-codec:v:0 copy";
             }
             }
 
 
-            const string keyFrameArg = " -force_key_frames expr:if(isnan(prev_forced_t),gte(t,.1),gte(t,prev_forced_t+5))";
+            var keyFrameArg = state.ReadInputAtNativeFramerate ?
+                " -force_key_frames expr:if(isnan(prev_forced_t),gte(t,.1),gte(t,prev_forced_t+1))" :
+                " -force_key_frames expr:if(isnan(prev_forced_t),gte(t,.1),gte(t,prev_forced_t+5))";
 
 
             var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream;
             var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream;
 
 

+ 1 - 4
MediaBrowser.Api/Playback/Hls/VideoHlsService.cs

@@ -1,19 +1,16 @@
-using System.Threading;
 using MediaBrowser.Common.IO;
 using MediaBrowser.Common.IO;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Dlna;
-using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
 using ServiceStack;
 using ServiceStack;
 using System;
 using System;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
+using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 
 
 namespace MediaBrowser.Api.Playback.Hls
 namespace MediaBrowser.Api.Playback.Hls

+ 1 - 1
MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs

@@ -134,7 +134,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
         /// <returns>System.String.</returns>
         /// <returns>System.String.</returns>
         public string GetProbeSizeArgument(string[] inputFiles, MediaProtocol protocol)
         public string GetProbeSizeArgument(string[] inputFiles, MediaProtocol protocol)
         {
         {
-            return EncodingUtils.GetProbeSizeArgument(inputFiles.Length > 0);
+            return EncodingUtils.GetProbeSizeArgument(inputFiles.Length > 1);
         }
         }
 
 
         /// <summary>
         /// <summary>

+ 4 - 2
MediaBrowser.Model/Dlna/StreamBuilder.cs

@@ -54,12 +54,14 @@ namespace MediaBrowser.Model.Dlna
                 // Avoid implicitly captured closure
                 // Avoid implicitly captured closure
                 string mediaSourceId = options.MediaSourceId;
                 string mediaSourceId = options.MediaSourceId;
 
 
-                mediaSources = new List<MediaSourceInfo>();
+                var newMediaSources = new List<MediaSourceInfo>();
                 foreach (MediaSourceInfo i in mediaSources)
                 foreach (MediaSourceInfo i in mediaSources)
                 {
                 {
                     if (StringHelper.EqualsIgnoreCase(i.Id, mediaSourceId))
                     if (StringHelper.EqualsIgnoreCase(i.Id, mediaSourceId))
-                        mediaSources.Add(i);
+                        newMediaSources.Add(i);
                 }
                 }
+
+                mediaSources = newMediaSources;
             }
             }
 
 
             List<StreamInfo> streams = new List<StreamInfo>();
             List<StreamInfo> streams = new List<StreamInfo>();

+ 170 - 26
MediaBrowser.Server.Implementations/Channels/ChannelManager.cs

@@ -1,4 +1,5 @@
-using MediaBrowser.Common.Extensions;
+using System.Collections.Concurrent;
+using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.IO;
 using MediaBrowser.Common.IO;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
@@ -23,7 +24,7 @@ using System.Threading.Tasks;
 
 
 namespace MediaBrowser.Server.Implementations.Channels
 namespace MediaBrowser.Server.Implementations.Channels
 {
 {
-    public class ChannelManager : IChannelManager
+    public class ChannelManager : IChannelManager, IDisposable
     {
     {
         private IChannel[] _channels;
         private IChannel[] _channels;
         private IChannelFactory[] _factories;
         private IChannelFactory[] _factories;
@@ -39,6 +40,9 @@ namespace MediaBrowser.Server.Implementations.Channels
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IJsonSerializer _jsonSerializer;
 
 
         private readonly ILocalizationManager _localization;
         private readonly ILocalizationManager _localization;
+        private readonly ConcurrentDictionary<Guid, bool> _refreshedItems = new ConcurrentDictionary<Guid, bool>();
+
+        private Timer _refreshTimer;
 
 
         public ChannelManager(IUserManager userManager, IDtoService dtoService, ILibraryManager libraryManager, ILogger logger, IServerConfigurationManager config, IFileSystem fileSystem, IUserDataManager userDataManager, IJsonSerializer jsonSerializer, ILocalizationManager localization)
         public ChannelManager(IUserManager userManager, IDtoService dtoService, ILibraryManager libraryManager, ILogger logger, IServerConfigurationManager config, IFileSystem fileSystem, IUserDataManager userDataManager, IJsonSerializer jsonSerializer, ILocalizationManager localization)
         {
         {
@@ -51,6 +55,8 @@ namespace MediaBrowser.Server.Implementations.Channels
             _userDataManager = userDataManager;
             _userDataManager = userDataManager;
             _jsonSerializer = jsonSerializer;
             _jsonSerializer = jsonSerializer;
             _localization = localization;
             _localization = localization;
+
+            _refreshTimer = new Timer(s => _refreshedItems.Clear(), null, TimeSpan.FromHours(3), TimeSpan.FromHours(3));
         }
         }
 
 
         private TimeSpan CacheLength
         private TimeSpan CacheLength
@@ -203,8 +209,8 @@ namespace MediaBrowser.Server.Implementations.Channels
 
 
             if (requiresCallback != null)
             if (requiresCallback != null)
             {
             {
-                results = await requiresCallback.GetChannelItemMediaInfo(item.ExternalId, cancellationToken)
-                   .ConfigureAwait(false);
+                results = await GetChannelItemMediaSourcesInternal(requiresCallback, item.ExternalId, cancellationToken)
+                            .ConfigureAwait(false);
             }
             }
             else
             else
             {
             {
@@ -221,6 +227,31 @@ namespace MediaBrowser.Server.Implementations.Channels
             return sources;
             return sources;
         }
         }
 
 
+        private readonly ConcurrentDictionary<string, Tuple<DateTime, List<ChannelMediaInfo>>> _channelItemMediaInfo =
+            new ConcurrentDictionary<string, Tuple<DateTime, List<ChannelMediaInfo>>>();
+
+        private async Task<IEnumerable<ChannelMediaInfo>> GetChannelItemMediaSourcesInternal(IRequiresMediaInfoCallback channel, string id, CancellationToken cancellationToken)
+        {
+            Tuple<DateTime, List<ChannelMediaInfo>> cachedInfo;
+
+            if (_channelItemMediaInfo.TryGetValue(id, out cachedInfo))
+            {
+                if ((DateTime.UtcNow - cachedInfo.Item1).TotalMinutes < 5)
+                {
+                    return cachedInfo.Item2;
+                }
+            }
+
+            var mediaInfo = await channel.GetChannelItemMediaInfo(id, cancellationToken)
+                   .ConfigureAwait(false);
+            var list = mediaInfo.ToList();
+
+            var item2 = new Tuple<DateTime, List<ChannelMediaInfo>>(DateTime.UtcNow, list);
+            _channelItemMediaInfo.AddOrUpdate(id, item2, (key, oldValue) => item2);
+
+            return list;
+        }
+
         public IEnumerable<MediaSourceInfo> GetCachedChannelItemMediaSources(string id)
         public IEnumerable<MediaSourceInfo> GetCachedChannelItemMediaSources(string id)
         {
         {
             var item = (IChannelMediaItem)_libraryManager.GetItemById(id);
             var item = (IChannelMediaItem)_libraryManager.GetItemById(id);
@@ -515,11 +546,7 @@ namespace MediaBrowser.Server.Implementations.Channels
                     {
                     {
                         try
                         try
                         {
                         {
-                            var result = await indexable.GetLatestMedia(new ChannelLatestMediaSearch
-                            {
-                                UserId = userId
-
-                            }, cancellationToken).ConfigureAwait(false);
+                            var result = await GetLatestItems(indexable, i, userId, cancellationToken).ConfigureAwait(false);
 
 
                             var resultItems = result.ToList();
                             var resultItems = result.ToList();
 
 
@@ -585,6 +612,65 @@ namespace MediaBrowser.Server.Implementations.Channels
             };
             };
         }
         }
 
 
+        private async Task<IEnumerable<ChannelItemInfo>> GetLatestItems(ISupportsLatestMedia indexable, IChannel channel, string userId, CancellationToken cancellationToken)
+        {
+            var cacheLength = TimeSpan.FromHours(12);
+            var cachePath = GetChannelDataCachePath(channel, userId, "channelmanager-latest", null, false);
+
+            try
+            {
+                if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
+                {
+                    return _jsonSerializer.DeserializeFromFile<List<ChannelItemInfo>>(cachePath);
+                }
+            }
+            catch (FileNotFoundException)
+            {
+
+            }
+            catch (DirectoryNotFoundException)
+            {
+
+            }
+
+            await _resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+            try
+            {
+                try
+                {
+                    if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
+                    {
+                        return _jsonSerializer.DeserializeFromFile<List<ChannelItemInfo>>(cachePath);
+                    }
+                }
+                catch (FileNotFoundException)
+                {
+
+                }
+                catch (DirectoryNotFoundException)
+                {
+
+                }
+
+                var result = await indexable.GetLatestMedia(new ChannelLatestMediaSearch
+                {
+                    UserId = userId
+
+                }, cancellationToken).ConfigureAwait(false);
+
+                var resultItems = result.ToList();
+
+                CacheResponse(resultItems, cachePath);
+
+                return resultItems;
+            }
+            finally
+            {
+                _resourcePool.Release();
+            }
+        }
+
         public async Task<QueryResult<BaseItemDto>> GetAllMedia(AllChannelMediaQuery query, CancellationToken cancellationToken)
         public async Task<QueryResult<BaseItemDto>> GetAllMedia(AllChannelMediaQuery query, CancellationToken cancellationToken)
         {
         {
             var user = string.IsNullOrWhiteSpace(query.UserId)
             var user = string.IsNullOrWhiteSpace(query.UserId)
@@ -614,11 +700,7 @@ namespace MediaBrowser.Server.Implementations.Channels
                     {
                     {
                         try
                         try
                         {
                         {
-                            var result = await indexable.GetAllMedia(new InternalAllChannelMediaQuery
-                            {
-                                UserId = userId
-
-                            }, cancellationToken).ConfigureAwait(false);
+                            var result = await GetAllItems(indexable, i, userId, cancellationToken).ConfigureAwait(false);
 
 
                             return new Tuple<IChannel, ChannelItemResult>(i, result);
                             return new Tuple<IChannel, ChannelItemResult>(i, result);
                         }
                         }
@@ -677,6 +759,63 @@ namespace MediaBrowser.Server.Implementations.Channels
             };
             };
         }
         }
 
 
+        private async Task<ChannelItemResult> GetAllItems(IIndexableChannel indexable, IChannel channel, string userId, CancellationToken cancellationToken)
+        {
+            var cacheLength = TimeSpan.FromHours(12);
+            var cachePath = GetChannelDataCachePath(channel, userId, "channelmanager-allitems", null, false);
+
+            try
+            {
+                if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
+                {
+                    return _jsonSerializer.DeserializeFromFile<ChannelItemResult>(cachePath);
+                }
+            }
+            catch (FileNotFoundException)
+            {
+
+            }
+            catch (DirectoryNotFoundException)
+            {
+
+            }
+
+            await _resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+            try
+            {
+                try
+                {
+                    if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
+                    {
+                        return _jsonSerializer.DeserializeFromFile<ChannelItemResult>(cachePath);
+                    }
+                }
+                catch (FileNotFoundException)
+                {
+
+                }
+                catch (DirectoryNotFoundException)
+                {
+
+                }
+
+                var result = await indexable.GetAllMedia(new InternalAllChannelMediaQuery
+                {
+                    UserId = userId
+
+                }, cancellationToken).ConfigureAwait(false);
+
+                CacheResponse(result, cachePath);
+
+                return result;
+            }
+            finally
+            {
+                _resourcePool.Release();
+            }
+        }
+
         public async Task<QueryResult<BaseItemDto>> GetChannelItems(ChannelItemQuery query, CancellationToken cancellationToken)
         public async Task<QueryResult<BaseItemDto>> GetChannelItems(ChannelItemQuery query, CancellationToken cancellationToken)
         {
         {
             var queryChannelId = query.ChannelId;
             var queryChannelId = query.ChannelId;
@@ -764,11 +903,9 @@ namespace MediaBrowser.Server.Implementations.Channels
             {
             {
                 if (!startIndex.HasValue && !limit.HasValue)
                 if (!startIndex.HasValue && !limit.HasValue)
                 {
                 {
-                    var channelItemResult = _jsonSerializer.DeserializeFromFile<ChannelItemResult>(cachePath);
-
                     if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
                     if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
                     {
                     {
-                        return channelItemResult;
+                        return _jsonSerializer.DeserializeFromFile<ChannelItemResult>(cachePath);
                     }
                     }
                 }
                 }
             }
             }
@@ -789,11 +926,9 @@ namespace MediaBrowser.Server.Implementations.Channels
                 {
                 {
                     if (!startIndex.HasValue && !limit.HasValue)
                     if (!startIndex.HasValue && !limit.HasValue)
                     {
                     {
-                        var channelItemResult = _jsonSerializer.DeserializeFromFile<ChannelItemResult>(cachePath);
-
                         if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
                         if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
                         {
                         {
-                            return channelItemResult;
+                            return _jsonSerializer.DeserializeFromFile<ChannelItemResult>(cachePath);
                         }
                         }
                     }
                     }
                 }
                 }
@@ -837,7 +972,7 @@ namespace MediaBrowser.Server.Implementations.Channels
             }
             }
         }
         }
 
 
-        private void CacheResponse(ChannelItemResult result, string path)
+        private void CacheResponse(object result, string path)
         {
         {
             try
             try
             {
             {
@@ -993,8 +1128,8 @@ namespace MediaBrowser.Server.Implementations.Channels
                 item.ProductionYear = info.ProductionYear;
                 item.ProductionYear = info.ProductionYear;
                 item.ProviderIds = info.ProviderIds;
                 item.ProviderIds = info.ProviderIds;
 
 
-                item.DateCreated = info.DateCreated.HasValue ? 
-                    info.DateCreated.Value : 
+                item.DateCreated = info.DateCreated.HasValue ?
+                    info.DateCreated.Value :
                     DateTime.UtcNow;
                     DateTime.UtcNow;
             }
             }
 
 
@@ -1042,14 +1177,14 @@ namespace MediaBrowser.Server.Implementations.Channels
 
 
         private async Task RefreshIfNeeded(BaseItem program, CancellationToken cancellationToken)
         private async Task RefreshIfNeeded(BaseItem program, CancellationToken cancellationToken)
         {
         {
-            //if (_refreshedPrograms.ContainsKey(program.Id))
+            if (_refreshedItems.ContainsKey(program.Id))
             {
             {
-                //return;
+                return;
             }
             }
 
 
             await program.RefreshMetadata(cancellationToken).ConfigureAwait(false);
             await program.RefreshMetadata(cancellationToken).ConfigureAwait(false);
 
 
-            //_refreshedPrograms.TryAdd(program.Id, true);
+            _refreshedItems.TryAdd(program.Id, true);
         }
         }
 
 
         internal IChannel GetChannelProvider(Channel channel)
         internal IChannel GetChannelProvider(Channel channel)
@@ -1155,5 +1290,14 @@ namespace MediaBrowser.Server.Implementations.Channels
             var name = _localization.GetLocalizedString("ViewTypeChannels");
             var name = _localization.GetLocalizedString("ViewTypeChannels");
             return await _libraryManager.GetNamedView(name, "channels", "zz_" + name, cancellationToken).ConfigureAwait(false);
             return await _libraryManager.GetNamedView(name, "channels", "zz_" + name, cancellationToken).ConfigureAwait(false);
         }
         }
+
+        public void Dispose()
+        {
+            if (_refreshTimer != null)
+            {
+                _refreshTimer.Dispose();
+                _refreshTimer = null;
+            }
+        }
     }
     }
 }
 }

+ 2 - 2
MediaBrowser.Server.Implementations/Library/LibraryManager.cs

@@ -1497,7 +1497,7 @@ namespace MediaBrowser.Server.Implementations.Library
 
 
         public async Task<UserView> GetNamedView(string name, string type, string sortName, CancellationToken cancellationToken)
         public async Task<UserView> GetNamedView(string name, string type, string sortName, CancellationToken cancellationToken)
         {
         {
-            var id = "namedview_2_" + name;
+            var id = "namedview_3_" + name;
             var guid = id.GetMD5();
             var guid = id.GetMD5();
 
 
             var item = GetItemById(guid) as UserView;
             var item = GetItemById(guid) as UserView;
@@ -1506,7 +1506,7 @@ namespace MediaBrowser.Server.Implementations.Library
             {
             {
                 var path = Path.Combine(ConfigurationManager.ApplicationPaths.ItemsByNamePath,
                 var path = Path.Combine(ConfigurationManager.ApplicationPaths.ItemsByNamePath,
                     "views",
                     "views",
-                    _fileSystem.GetValidFilename(name));
+                    _fileSystem.GetValidFilename(type));
 
 
                 Directory.CreateDirectory(Path.GetDirectoryName(path));
                 Directory.CreateDirectory(Path.GetDirectoryName(path));
 
 

+ 12 - 3
MediaBrowser.ServerApplication/MainStartup.cs

@@ -527,18 +527,27 @@ namespace MediaBrowser.ServerApplication
 
 
             if (!_isRunningAsService)
             if (!_isRunningAsService)
             {
             {
-                _logger.Info("Executing windows forms restart");
+                _logger.Info("Hiding server notify icon");
                 _serverNotifyIcon.Visible = false;
                 _serverNotifyIcon.Visible = false;
-                Application.Restart();
 
 
-                ShutdownWindowsApplication();
+                _logger.Info("Executing windows forms restart");
+                //Application.Restart();
+                Process.Start(_appHost.ServerConfigurationManager.ApplicationPaths.ApplicationPath);
+
+                _logger.Info("Calling Application.Exit");
+                Environment.Exit(0);
             }
             }
         }
         }
 
 
         private static void ShutdownWindowsApplication()
         private static void ShutdownWindowsApplication()
         {
         {
+            _logger.Info("Hiding server notify icon");
             _serverNotifyIcon.Visible = false;
             _serverNotifyIcon.Visible = false;
+
+            _logger.Info("Calling Application.Exit");
             Application.Exit();
             Application.Exit();
+
+            _logger.Info("Calling ApplicationTaskCompletionSource.SetResult");
             ApplicationTaskCompletionSource.SetResult(true);
             ApplicationTaskCompletionSource.SetResult(true);
         }
         }
 
 

+ 2 - 2
MediaBrowser.WebDashboard/Api/DashboardService.cs

@@ -311,6 +311,7 @@ namespace MediaBrowser.WebDashboard.Api
         /// Modifies the HTML by adding common meta tags, css and js.
         /// Modifies the HTML by adding common meta tags, css and js.
         /// </summary>
         /// </summary>
         /// <param name="sourceStream">The source stream.</param>
         /// <param name="sourceStream">The source stream.</param>
+        /// <param name="userId">The user identifier.</param>
         /// <param name="localizationCulture">The localization culture.</param>
         /// <param name="localizationCulture">The localization culture.</param>
         /// <returns>Task{Stream}.</returns>
         /// <returns>Task{Stream}.</returns>
         private async Task<Stream> ModifyHtml(Stream sourceStream, string localizationCulture)
         private async Task<Stream> ModifyHtml(Stream sourceStream, string localizationCulture)
@@ -373,8 +374,7 @@ namespace MediaBrowser.WebDashboard.Api
             sb.Append("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, user-scalable=no\">");
             sb.Append("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, user-scalable=no\">");
             sb.Append("<meta name=\"apple-mobile-web-app-capable\" content=\"yes\">");
             sb.Append("<meta name=\"apple-mobile-web-app-capable\" content=\"yes\">");
             sb.Append("<meta name=\"mobile-web-app-capable\" content=\"yes\">");
             sb.Append("<meta name=\"mobile-web-app-capable\" content=\"yes\">");
-            //sb.Append("<meta name=\"application-name\" content=\"Media Browser\">");
-            //sb.Append("<meta name=\"msapplication-config\" content=\"config.xml\">");
+            sb.Append("<meta name=\"application-name\" content=\"Media Browser\">");
             //sb.Append("<meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black-translucent\">");
             //sb.Append("<meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black-translucent\">");
 
 
             sb.Append("<link rel=\"icon\" sizes=\"114x114\" href=\"css/images/touchicon114.png\" />");
             sb.Append("<link rel=\"icon\" sizes=\"114x114\" href=\"css/images/touchicon114.png\" />");

+ 2 - 2
Nuget/MediaBrowser.Common.Internal.nuspec

@@ -2,7 +2,7 @@
 <package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
 <package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
     <metadata>
     <metadata>
         <id>MediaBrowser.Common.Internal</id>
         <id>MediaBrowser.Common.Internal</id>
-        <version>3.0.408</version>
+        <version>3.0.409</version>
         <title>MediaBrowser.Common.Internal</title>
         <title>MediaBrowser.Common.Internal</title>
         <authors>Luke</authors>
         <authors>Luke</authors>
         <owners>ebr,Luke,scottisafool</owners>
         <owners>ebr,Luke,scottisafool</owners>
@@ -12,7 +12,7 @@
         <description>Contains common components shared by Media Browser Theater and Media Browser Server. Not intended for plugin developer consumption.</description>
         <description>Contains common components shared by Media Browser Theater and Media Browser Server. Not intended for plugin developer consumption.</description>
         <copyright>Copyright © Media Browser 2013</copyright>
         <copyright>Copyright © Media Browser 2013</copyright>
         <dependencies>
         <dependencies>
-            <dependency id="MediaBrowser.Common" version="3.0.408" />
+            <dependency id="MediaBrowser.Common" version="3.0.409" />
             <dependency id="NLog" version="2.1.0" />
             <dependency id="NLog" version="2.1.0" />
             <dependency id="SimpleInjector" version="2.5.0" />
             <dependency id="SimpleInjector" version="2.5.0" />
             <dependency id="sharpcompress" version="0.10.2" />
             <dependency id="sharpcompress" version="0.10.2" />

+ 1 - 1
Nuget/MediaBrowser.Common.nuspec

@@ -2,7 +2,7 @@
 <package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
 <package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
     <metadata>
     <metadata>
         <id>MediaBrowser.Common</id>
         <id>MediaBrowser.Common</id>
-        <version>3.0.408</version>
+        <version>3.0.409</version>
         <title>MediaBrowser.Common</title>
         <title>MediaBrowser.Common</title>
         <authors>Media Browser Team</authors>
         <authors>Media Browser Team</authors>
         <owners>ebr,Luke,scottisafool</owners>
         <owners>ebr,Luke,scottisafool</owners>

+ 2 - 2
Nuget/MediaBrowser.Server.Core.nuspec

@@ -2,7 +2,7 @@
 <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
 <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
     <metadata>
     <metadata>
         <id>MediaBrowser.Server.Core</id>
         <id>MediaBrowser.Server.Core</id>
-        <version>3.0.408</version>
+        <version>3.0.409</version>
         <title>Media Browser.Server.Core</title>
         <title>Media Browser.Server.Core</title>
         <authors>Media Browser Team</authors>
         <authors>Media Browser Team</authors>
         <owners>ebr,Luke,scottisafool</owners>
         <owners>ebr,Luke,scottisafool</owners>
@@ -12,7 +12,7 @@
         <description>Contains core components required to build plugins for Media Browser Server.</description>
         <description>Contains core components required to build plugins for Media Browser Server.</description>
         <copyright>Copyright © Media Browser 2013</copyright>
         <copyright>Copyright © Media Browser 2013</copyright>
         <dependencies>
         <dependencies>
-            <dependency id="MediaBrowser.Common" version="3.0.408" />
+            <dependency id="MediaBrowser.Common" version="3.0.409" />
         </dependencies>
         </dependencies>
     </metadata>
     </metadata>
     <files>
     <files>