Browse Source

Merge pull request #2554 from MediaBrowser/beta

Beta
Luke 8 years ago
parent
commit
88e3fcfdc7
100 changed files with 1926 additions and 1640 deletions
  1. 1 2
      Emby.Common.Implementations/IO/LnkShortcutHandler.cs
  2. 10 5
      Emby.Common.Implementations/IO/ManagedFileSystem.cs
  3. 42 1
      Emby.Common.Implementations/Net/NetAcceptSocket.cs
  4. 37 3
      Emby.Common.Implementations/Net/SocketFactory.cs
  5. 138 68
      Emby.Common.Implementations/Net/UdpSocket.cs
  6. 2 2
      Emby.Dlna/Didl/DidlBuilder.cs
  7. 1 1
      Emby.Dlna/PlayTo/PlayToController.cs
  8. 10 10
      Emby.Drawing/ImageProcessor.cs
  9. 39 17
      Emby.Server.Core/ApplicationHost.cs
  10. 3 1
      Emby.Server.Core/HttpServerFactory.cs
  11. 0 11
      Emby.Server.Core/IO/LibraryMonitor.cs
  12. 28 4
      Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs
  13. 60 19
      Emby.Server.Implementations/Data/SqliteItemRepository.cs
  14. 2 2
      Emby.Server.Implementations/Dto/DtoService.cs
  15. 3 5
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  16. 8 38
      Emby.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs
  17. 191 0
      Emby.Server.Implementations/HttpServer/FileWriter.cs
  18. 0 17
      Emby.Server.Implementations/HttpServer/GetSwaggerResource.cs
  19. 5 3
      Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
  20. 26 16
      Emby.Server.Implementations/HttpServer/HttpResultFactory.cs
  21. 4 2
      Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs
  22. 8 0
      Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs
  23. 0 47
      Emby.Server.Implementations/HttpServer/SwaggerService.cs
  24. 1 1
      Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
  25. 17 10
      Emby.Server.Implementations/Library/LibraryManager.cs
  26. 2 0
      Emby.Server.Implementations/Library/MediaSourceManager.cs
  27. 146 7
      Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
  28. 6 2
      Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
  29. 5 0
      Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
  30. 9 0
      Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
  31. 38 62
      Emby.Server.Implementations/LiveTv/LiveTvManager.cs
  32. 1 1
      Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs
  33. 24 5
      Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
  34. 0 158
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunDiscovery.cs
  35. 128 128
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
  36. 6 7
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
  37. 5 12
      Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
  38. 65 4
      Emby.Server.Implementations/LiveTv/TunerHosts/MulticastStream.cs
  39. 43 14
      Emby.Server.Implementations/LiveTv/TunerHosts/QueueStream.cs
  40. 1 0
      Emby.Server.Implementations/Localization/LocalizationManager.cs
  41. 32 0
      Emby.Server.Implementations/Playlists/PlaylistImageProvider.cs
  42. 2 0
      Emby.Server.Implementations/Security/PluginSecurityManager.cs
  43. 39 43
      Emby.Server.Implementations/Services/ResponseHelper.cs
  44. 17 12
      Emby.Server.Implementations/TV/TVSeriesManager.cs
  45. 11 15
      Emby.Server.Implementations/Udp/UdpServer.cs
  46. 1 1
      Emby.Server.Implementations/packages.config
  47. 33 33
      MediaBrowser.Api/LiveTv/LiveTvService.cs
  48. 17 129
      MediaBrowser.Api/Playback/BaseStreamingService.cs
  49. 30 10
      MediaBrowser.Api/Playback/Hls/BaseHlsService.cs
  50. 16 25
      MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs
  51. 5 3
      MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs
  52. 1 11
      MediaBrowser.Api/Playback/Hls/VideoHlsService.cs
  53. 38 30
      MediaBrowser.Api/Playback/MediaInfoService.cs
  54. 2 4
      MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs
  55. 1 183
      MediaBrowser.Api/Playback/Progressive/VideoService.cs
  56. 4 2
      MediaBrowser.Api/Playback/StreamRequest.cs
  57. 26 4
      MediaBrowser.Api/Playback/StreamState.cs
  58. 3 73
      MediaBrowser.Api/StartupWizardService.cs
  59. 8 1
      MediaBrowser.Api/TvShowsService.cs
  60. 6 1
      MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
  61. 6 1
      MediaBrowser.Controller/Entities/Audio/MusicGenre.cs
  62. 6 1
      MediaBrowser.Controller/Entities/GameGenre.cs
  63. 6 1
      MediaBrowser.Controller/Entities/Genre.cs
  64. 2 0
      MediaBrowser.Controller/Entities/IHasImages.cs
  65. 6 1
      MediaBrowser.Controller/Entities/Person.cs
  66. 6 1
      MediaBrowser.Controller/Entities/Studio.cs
  67. 2 1
      MediaBrowser.Controller/Entities/Video.cs
  68. 6 1
      MediaBrowser.Controller/Entities/Year.cs
  69. 3 8
      MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
  70. 2 0
      MediaBrowser.Controller/LiveTv/ITunerHost.cs
  71. 1 0
      MediaBrowser.Controller/MediaBrowser.Controller.csproj
  72. 233 4
      MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
  73. 9 2
      MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
  74. 8 2
      MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs
  75. 37 10
      MediaBrowser.Controller/MediaEncoding/JobLogger.cs
  76. 5 8
      MediaBrowser.Controller/Net/StaticResultOptions.cs
  77. 2 2
      MediaBrowser.MediaEncoding/Encoder/AudioEncoder.cs
  78. 4 4
      MediaBrowser.MediaEncoding/Encoder/BaseEncoder.cs
  79. 1 0
      MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
  80. 3 5
      MediaBrowser.MediaEncoding/Encoder/EncodingJob.cs
  81. 10 1
      MediaBrowser.MediaEncoding/Encoder/EncodingJobFactory.cs
  82. 3 159
      MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
  83. 2 133
      MediaBrowser.MediaEncoding/Encoder/VideoEncoder.cs
  84. 0 1
      MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
  85. 2 0
      MediaBrowser.MediaEncoding/Probing/InternalMediaInfoResult.cs
  86. 5 0
      MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
  87. 7 12
      MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
  88. 2 0
      MediaBrowser.Model/Configuration/LibraryOptions.cs
  89. 1 0
      MediaBrowser.Model/Configuration/ServerConfiguration.cs
  90. 2 1
      MediaBrowser.Model/Dlna/ProfileConditionValue.cs
  91. 55 8
      MediaBrowser.Model/Dlna/StreamBuilder.cs
  92. 61 5
      MediaBrowser.Model/Dlna/StreamInfo.cs
  93. 6 0
      MediaBrowser.Model/Dlna/TranscodingProfile.cs
  94. 2 0
      MediaBrowser.Model/IO/IFileSystem.cs
  95. 2 3
      MediaBrowser.Model/LiveTv/LiveTvOptions.cs
  96. 2 0
      MediaBrowser.Model/MediaInfo/PlaybackInfoRequest.cs
  97. 3 0
      MediaBrowser.Model/Net/IAcceptSocket.cs
  98. 1 0
      MediaBrowser.Model/Net/ISocket.cs
  99. 2 0
      MediaBrowser.Model/Net/ISocketFactory.cs
  100. 3 0
      MediaBrowser.Model/Querying/NextUpQuery.cs

+ 1 - 2
MediaBrowser.ServerApplication/Native/LnkShortcutHandler.cs → Emby.Common.Implementations/IO/LnkShortcutHandler.cs

@@ -6,7 +6,7 @@ using System.Security;
 using System.Text;
 using MediaBrowser.Model.IO;
 
-namespace MediaBrowser.ServerApplication.Native
+namespace Emby.Common.Implementations.IO
 {
     public class LnkShortcutHandler :IShortcutHandler
     {
@@ -35,7 +35,6 @@ namespace MediaBrowser.ServerApplication.Native
     /// <summary>
     /// Class NativeMethods
     /// </summary>
-    [SuppressUnmanagedCodeSecurity]
     public static class NativeMethods
     {
         /// <summary>

+ 10 - 5
Emby.Common.Implementations/IO/ManagedFileSystem.cs

@@ -5,6 +5,7 @@ using System.Linq;
 using System.Text;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.System;
 
 namespace Emby.Common.Implementations.IO
 {
@@ -18,17 +19,21 @@ namespace Emby.Common.Implementations.IO
         private readonly bool _supportsAsyncFileStreams;
         private char[] _invalidFileNameChars;
         private readonly List<IShortcutHandler> _shortcutHandlers = new List<IShortcutHandler>();
-        private bool EnableFileSystemRequestConcat = true;
+        private bool EnableFileSystemRequestConcat;
 
         private string _tempPath;
 
-        public ManagedFileSystem(ILogger logger, bool supportsAsyncFileStreams, bool enableManagedInvalidFileNameChars, bool enableFileSystemRequestConcat, string tempPath)
+        public ManagedFileSystem(ILogger logger, IEnvironmentInfo environmentInfo, string tempPath)
         {
             Logger = logger;
-            _supportsAsyncFileStreams = supportsAsyncFileStreams;
+            _supportsAsyncFileStreams = true;
             _tempPath = tempPath;
-            EnableFileSystemRequestConcat = enableFileSystemRequestConcat;
-            SetInvalidFileNameChars(enableManagedInvalidFileNameChars);
+
+            // On Linux, this needs to be true or symbolic links are ignored
+            EnableFileSystemRequestConcat = environmentInfo.OperatingSystem != MediaBrowser.Model.System.OperatingSystem.Windows &&
+                environmentInfo.OperatingSystem != MediaBrowser.Model.System.OperatingSystem.OSX;
+
+            SetInvalidFileNameChars(environmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows);
         }
 
         public void AddShortcutHandler(IShortcutHandler handler)

+ 42 - 1
Emby.Common.Implementations/Net/NetAcceptSocket.cs

@@ -2,6 +2,7 @@
 using System.Net;
 using System.Net.Sockets;
 using System.Threading;
+using System.Threading.Tasks;
 using Emby.Common.Implementations.Networking;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Logging;
@@ -59,7 +60,7 @@ namespace Emby.Common.Implementations.Net
 #if NET46
             Socket.Close();
 #else
-                        Socket.Dispose();
+            Socket.Dispose();
 #endif
         }
 
@@ -96,6 +97,46 @@ namespace Emby.Common.Implementations.Net
             _acceptor.StartAccept();
         }
 
+#if NET46
+        public Task SendFile(string path, byte[] preBuffer, byte[] postBuffer, CancellationToken cancellationToken)
+        {
+            var options = TransmitFileOptions.UseKernelApc;
+
+            var completionSource = new TaskCompletionSource<bool>();
+
+            var result = Socket.BeginSendFile(path, preBuffer, postBuffer, options, new AsyncCallback(FileSendCallback), new Tuple<Socket, string, TaskCompletionSource<bool>>(Socket, path, completionSource));
+
+            return completionSource.Task;
+        }
+
+        private void FileSendCallback(IAsyncResult ar)
+        {
+            // Retrieve the socket from the state object.
+            Tuple<Socket, string, TaskCompletionSource<bool>> data = (Tuple<Socket, string, TaskCompletionSource<bool>>)ar.AsyncState;
+
+            var client = data.Item1;
+            var path = data.Item2;
+            var taskCompletion = data.Item3;
+        
+            // Complete sending the data to the remote device.
+        try {
+            client.EndSendFile(ar);
+        taskCompletion.TrySetResult(true);
+}
+        catch(SocketException ex){
+        _logger.Info("Socket.SendFile failed for {0}. error code {1}", path, ex.SocketErrorCode);
+        taskCompletion.TrySetException(ex);
+}catch(Exception ex){
+        taskCompletion.TrySetException(ex);
+}
+        }
+#else
+        public Task SendFile(string path, byte[] preBuffer, byte[] postBuffer, CancellationToken cancellationToken)
+        {
+            throw new NotImplementedException();
+        }
+#endif
+
         public void Dispose()
         {
             Socket.Dispose();

+ 37 - 3
Emby.Common.Implementations/Net/SocketFactory.cs

@@ -52,6 +52,18 @@ namespace Emby.Common.Implementations.Net
             {
                 throw new SocketCreateException(ex.SocketErrorCode.ToString(), ex);
             }
+            catch (ArgumentException ex)
+            {
+                if (dualMode)
+                {
+                    // Mono for BSD incorrectly throws ArgumentException instead of SocketException
+                    throw new SocketCreateException("AddressFamilyNotSupported", ex);
+                }
+                else
+                {
+                    throw;
+                }
+            }
         }
 
         public ISocket CreateTcpSocket(IpAddressInfo remoteAddress, int remotePort)
@@ -59,6 +71,7 @@ namespace Emby.Common.Implementations.Net
             if (remotePort < 0) throw new ArgumentException("remotePort cannot be less than zero.", "remotePort");
 
             var retVal = new Socket(AddressFamily.InterNetwork, System.Net.Sockets.SocketType.Stream, System.Net.Sockets.ProtocolType.Tcp);
+            
             try
             {
                 retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
@@ -96,10 +109,31 @@ namespace Emby.Common.Implementations.Net
             }
         }
 
+        public ISocket CreateUdpBroadcastSocket(int localPort)
+        {
+            if (localPort < 0) throw new ArgumentException("localPort cannot be less than zero.", "localPort");
+
+            var retVal = new Socket(AddressFamily.InterNetwork, System.Net.Sockets.SocketType.Dgram, System.Net.Sockets.ProtocolType.Udp);
+            try
+            {
+                retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
+                retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, 1);
+
+                return new UdpSocket(retVal, localPort, IPAddress.Any);
+            }
+            catch
+            {
+                if (retVal != null)
+                    retVal.Dispose();
+
+                throw;
+            }
+        }
+        
         /// <summary>
-        /// Creates a new UDP acceptSocket that is a member of the SSDP multicast local admin group and binds it to the specified local port.
-        /// </summary>
-        /// <returns>An implementation of the <see cref="ISocket"/> interface used by RSSDP components to perform acceptSocket operations.</returns>
+              /// Creates a new UDP acceptSocket that is a member of the SSDP multicast local admin group and binds it to the specified local port.
+              /// </summary>
+              /// <returns>An implementation of the <see cref="ISocket"/> interface used by RSSDP components to perform acceptSocket operations.</returns>
         public ISocket CreateSsdpUdpSocket(IpAddressInfo localIpAddress, int localPort)
         {
             if (localPort < 0) throw new ArgumentException("localPort cannot be less than zero.", "localPort");

+ 138 - 68
Emby.Common.Implementations/Net/UdpSocket.cs

@@ -16,12 +16,23 @@ namespace Emby.Common.Implementations.Net
 
     internal sealed class UdpSocket : DisposableManagedObjectBase, ISocket
     {
-
-        #region Fields
-
         private Socket _Socket;
         private int _LocalPort;
-        #endregion
+
+        private readonly SocketAsyncEventArgs _receiveSocketAsyncEventArgs = new SocketAsyncEventArgs()
+        {
+            SocketFlags = SocketFlags.None
+        };
+
+        private readonly SocketAsyncEventArgs _sendSocketAsyncEventArgs = new SocketAsyncEventArgs()
+        {
+            SocketFlags = SocketFlags.None
+        };
+
+        private TaskCompletionSource<SocketReceiveResult> _currentReceiveTaskCompletionSource;
+        private TaskCompletionSource<int> _currentSendTaskCompletionSource;
+
+        private readonly SemaphoreSlim _sendLock = new SemaphoreSlim(1, 1);
 
         public UdpSocket(Socket socket, int localPort, IPAddress ip)
         {
@@ -32,6 +43,61 @@ namespace Emby.Common.Implementations.Net
             LocalIPAddress = NetworkManager.ToIpAddressInfo(ip);
 
             _Socket.Bind(new IPEndPoint(ip, _LocalPort));
+
+            InitReceiveSocketAsyncEventArgs();
+        }
+
+        private void InitReceiveSocketAsyncEventArgs()
+        {
+            var receiveBuffer = new byte[8192];
+            _receiveSocketAsyncEventArgs.SetBuffer(receiveBuffer, 0, receiveBuffer.Length);
+            _receiveSocketAsyncEventArgs.Completed += _receiveSocketAsyncEventArgs_Completed;
+
+            var sendBuffer = new byte[8192];
+            _sendSocketAsyncEventArgs.SetBuffer(sendBuffer, 0, sendBuffer.Length);
+            _sendSocketAsyncEventArgs.Completed += _sendSocketAsyncEventArgs_Completed;
+        }
+
+        private void _receiveSocketAsyncEventArgs_Completed(object sender, SocketAsyncEventArgs e)
+        {
+            var tcs = _currentReceiveTaskCompletionSource;
+            if (tcs != null)
+            {
+                _currentReceiveTaskCompletionSource = null;
+
+                if (e.SocketError == SocketError.Success)
+                {
+                    tcs.TrySetResult(new SocketReceiveResult
+                    {
+                        Buffer = e.Buffer,
+                        ReceivedBytes = e.BytesTransferred,
+                        RemoteEndPoint = ToIpEndPointInfo(e.RemoteEndPoint as IPEndPoint),
+                        LocalIPAddress = LocalIPAddress
+                    });
+                }
+                else
+                {
+                    tcs.TrySetException(new Exception("SocketError: " + e.SocketError));
+                }
+            }
+        }
+
+        private void _sendSocketAsyncEventArgs_Completed(object sender, SocketAsyncEventArgs e)
+        {
+            var tcs = _currentSendTaskCompletionSource;
+            if (tcs != null)
+            {
+                _currentSendTaskCompletionSource = null;
+
+                if (e.SocketError == SocketError.Success)
+                {
+                    tcs.TrySetResult(e.BytesTransferred);
+                }
+                else
+                {
+                    tcs.TrySetException(new Exception("SocketError: " + e.SocketError));
+                }
+            }
         }
 
         public UdpSocket(Socket socket, IpEndPointInfo endPoint)
@@ -40,6 +106,8 @@ namespace Emby.Common.Implementations.Net
 
             _Socket = socket;
             _Socket.Connect(NetworkManager.ToIPEndPoint(endPoint));
+
+            InitReceiveSocketAsyncEventArgs();
         }
 
         public IpAddressInfo LocalIPAddress
@@ -48,32 +116,33 @@ namespace Emby.Common.Implementations.Net
             private set;
         }
 
-        #region ISocket Members
-
         public Task<SocketReceiveResult> ReceiveAsync(CancellationToken cancellationToken)
         {
             ThrowIfDisposed();
-
             var tcs = new TaskCompletionSource<SocketReceiveResult>();
-
             EndPoint receivedFromEndPoint = new IPEndPoint(IPAddress.Any, 0);
+
             var state = new AsyncReceiveState(_Socket, receivedFromEndPoint);
             state.TaskCompletionSource = tcs;
 
-#if NETSTANDARD1_6
-            _Socket.ReceiveFromAsync(new ArraySegment<Byte>(state.Buffer), SocketFlags.None, state.RemoteEndPoint)
-                .ContinueWith((task, asyncState) =>
+            cancellationToken.Register(() => tcs.TrySetCanceled());
+
+            _receiveSocketAsyncEventArgs.RemoteEndPoint = receivedFromEndPoint;
+            _currentReceiveTaskCompletionSource = tcs;
+
+            try
+            {
+                var willRaiseEvent = _Socket.ReceiveFromAsync(_receiveSocketAsyncEventArgs);
+
+                if (!willRaiseEvent)
                 {
-                    if (task.Status != TaskStatus.Faulted)
-                    {
-                        var receiveState = asyncState as AsyncReceiveState;
-                        receiveState.RemoteEndPoint = task.Result.RemoteEndPoint;
-                        ProcessResponse(receiveState, () => task.Result.ReceivedBytes, LocalIPAddress);
-                    }
-                }, state);
-#else
-            _Socket.BeginReceiveFrom(state.Buffer, 0, state.Buffer.Length, SocketFlags.None, ref state.RemoteEndPoint, ProcessResponse, state);
-#endif
+                    _receiveSocketAsyncEventArgs_Completed(this, _receiveSocketAsyncEventArgs);
+                }
+            }
+            catch (Exception ex)
+            {
+                tcs.TrySetException(ex);
+            }
 
             return tcs.Task;
         }
@@ -129,61 +198,69 @@ namespace Emby.Common.Implementations.Net
                 taskSource.TrySetException(ex);
             }
 
-            //_Socket.SendTo(messageData, new System.Net.IPEndPoint(IPAddress.Parse(RemoteEndPoint.IPAddress), RemoteEndPoint.Port));
-
             return taskSource.Task;
 #endif
-        }
+            //ThrowIfDisposed();
 
-        #endregion
+            //if (buffer == null) throw new ArgumentNullException("messageData");
+            //if (endPoint == null) throw new ArgumentNullException("endPoint");
 
-        #region Overrides
+            //cancellationToken.ThrowIfCancellationRequested();
 
-        protected override void Dispose(bool disposing)
-        {
-            if (disposing)
-            {
-                var socket = _Socket;
-                if (socket != null)
-                    socket.Dispose();
-            }
-        }
+            //var tcs = new TaskCompletionSource<int>();
+
+            //cancellationToken.Register(() => tcs.TrySetCanceled());
+
+            //_sendSocketAsyncEventArgs.SetBuffer(buffer, 0, size);
+            //_sendSocketAsyncEventArgs.RemoteEndPoint = NetworkManager.ToIPEndPoint(endPoint);
+            //_currentSendTaskCompletionSource = tcs;
 
-        #endregion
+            //var willRaiseEvent = _Socket.SendAsync(_sendSocketAsyncEventArgs);
 
-        #region Private Methods
+            //if (!willRaiseEvent)
+            //{
+            //    _sendSocketAsyncEventArgs_Completed(this, _sendSocketAsyncEventArgs);
+            //}
 
-        private static void ProcessResponse(AsyncReceiveState state, Func<int> receiveData, IpAddressInfo localIpAddress)
+            //return tcs.Task;
+        }
+
+        public async Task SendWithLockAsync(byte[] buffer, int size, IpEndPointInfo endPoint, CancellationToken cancellationToken)
         {
-            try
-            {
-                var bytesRead = receiveData();
+            ThrowIfDisposed();
 
-                var ipEndPoint = state.RemoteEndPoint as IPEndPoint;
-                state.TaskCompletionSource.SetResult(
-                    new SocketReceiveResult
-                    {
-                        Buffer = state.Buffer,
-                        ReceivedBytes = bytesRead,
-                        RemoteEndPoint = ToIpEndPointInfo(ipEndPoint),
-                        LocalIPAddress = localIpAddress
-                    }
-                );
-            }
-            catch (ObjectDisposedException)
+            //await _sendLock.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+            try
             {
-                state.TaskCompletionSource.SetCanceled();
+                await SendAsync(buffer, size, endPoint, cancellationToken).ConfigureAwait(false);
             }
-            catch (SocketException se)
+            finally
             {
-                if (se.SocketErrorCode != SocketError.Interrupted && se.SocketErrorCode != SocketError.OperationAborted && se.SocketErrorCode != SocketError.Shutdown)
-                    state.TaskCompletionSource.SetException(se);
-                else
-                    state.TaskCompletionSource.SetCanceled();
+                //_sendLock.Release();
             }
-            catch (Exception ex)
+        }
+
+        protected override void Dispose(bool disposing)
+        {
+            if (disposing)
             {
-                state.TaskCompletionSource.SetException(ex);
+                var socket = _Socket;
+                if (socket != null)
+                    socket.Dispose();
+
+                _sendLock.Dispose();
+
+                var tcs = _currentReceiveTaskCompletionSource;
+                if (tcs != null)
+                {
+                    tcs.TrySetCanceled();
+                }
+                var sendTcs = _currentSendTaskCompletionSource;
+                if (sendTcs != null)
+                {
+                    sendTcs.TrySetCanceled();
+                }
             }
         }
 
@@ -227,10 +304,6 @@ namespace Emby.Common.Implementations.Net
 #endif
         }
 
-        #endregion
-
-        #region Private Classes
-
         private class AsyncReceiveState
         {
             public AsyncReceiveState(Socket socket, EndPoint remoteEndPoint)
@@ -247,8 +320,5 @@ namespace Emby.Common.Implementations.Net
             public TaskCompletionSource<SocketReceiveResult> TaskCompletionSource { get; set; }
 
         }
-
-        #endregion
-
     }
 }

+ 2 - 2
Emby.Dlna/Didl/DidlBuilder.cs

@@ -208,7 +208,7 @@ namespace Emby.Dlna.Didl
             var targetHeight = streamInfo.TargetHeight;
 
             var contentFeatureList = new ContentFeatureBuilder(_profile).BuildVideoHeader(streamInfo.Container,
-                streamInfo.VideoCodec,
+                streamInfo.TargetVideoCodec,
                 streamInfo.TargetAudioCodec,
                 targetWidth,
                 targetHeight,
@@ -352,7 +352,7 @@ namespace Emby.Dlna.Didl
 
             var mediaProfile = _profile.GetVideoMediaProfile(streamInfo.Container,
                 streamInfo.TargetAudioCodec,
-                streamInfo.VideoCodec,
+                streamInfo.TargetVideoCodec,
                 streamInfo.TargetAudioBitrate,
                 targetWidth,
                 targetHeight,

+ 1 - 1
Emby.Dlna/PlayTo/PlayToController.cs

@@ -542,7 +542,7 @@ namespace Emby.Dlna.PlayTo
             {
                 var list = new ContentFeatureBuilder(profile)
                     .BuildVideoHeader(streamInfo.Container,
-                    streamInfo.VideoCodec,
+                    streamInfo.TargetVideoCodec,
                     streamInfo.TargetAudioCodec,
                     streamInfo.TargetWidth,
                     streamInfo.TargetHeight,

+ 10 - 10
Emby.Drawing/ImageProcessor.cs

@@ -238,7 +238,7 @@ namespace Emby.Drawing
             var outputFormat = GetOutputFormat(options.SupportedOutputFormats[0]);
             var cacheFilePath = GetCacheFilePath(originalImagePath, newSize, quality, dateModified, outputFormat, options.AddPlayedIndicator, options.PercentPlayed, options.UnplayedCount, options.Blur, options.BackgroundColor, options.ForegroundLayer);
 
-            var imageProcessingLockTaken = false;
+            //var imageProcessingLockTaken = false;
 
             try
             {
@@ -253,9 +253,9 @@ namespace Emby.Drawing
                     var tmpPath = Path.ChangeExtension(Path.Combine(_appPaths.TempDirectory, Guid.NewGuid().ToString("N")), Path.GetExtension(cacheFilePath));
                     _fileSystem.CreateDirectory(Path.GetDirectoryName(tmpPath));
 
-                    await _imageProcessingSemaphore.WaitAsync().ConfigureAwait(false);
+                    //await _imageProcessingSemaphore.WaitAsync().ConfigureAwait(false);
 
-                    imageProcessingLockTaken = true;
+                    //imageProcessingLockTaken = true;
 
                     _imageEncoder.EncodeImage(originalImagePath, tmpPath, AutoOrient(options.Item), newWidth, newHeight, quality, options, outputFormat);
                     CopyFile(tmpPath, cacheFilePath);
@@ -273,13 +273,13 @@ namespace Emby.Drawing
                 // Just spit out the original file if all the options are default
                 return new Tuple<string, string, DateTime>(originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
             }
-            finally
-            {
-                if (imageProcessingLockTaken)
-                {
-                    _imageProcessingSemaphore.Release();
-                }
-            }
+            //finally
+            //{
+            //    if (imageProcessingLockTaken)
+            //    {
+            //        _imageProcessingSemaphore.Release();
+            //    }
+            //}
         }
 
         private void CopyFile(string src, string destination)

+ 39 - 17
Emby.Server.Core/ApplicationHost.cs

@@ -61,7 +61,7 @@ using System.Threading;
 using System.Threading.Tasks;
 using Emby.Common.Implementations;
 using Emby.Common.Implementations.Archiving;
-using Emby.Common.Implementations.Networking;
+using Emby.Common.Implementations.IO;
 using Emby.Common.Implementations.Reflection;
 using Emby.Common.Implementations.Serialization;
 using Emby.Common.Implementations.TextEncoding;
@@ -93,7 +93,7 @@ using Emby.Server.Implementations.Social;
 using Emby.Server.Implementations.Channels;
 using Emby.Server.Implementations.Collections;
 using Emby.Server.Implementations.Dto;
-using Emby.Server.Implementations.EntryPoints;
+using Emby.Server.Implementations.IO;
 using Emby.Server.Implementations.FileOrganization;
 using Emby.Server.Implementations.HttpServer;
 using Emby.Server.Implementations.HttpServer.Security;
@@ -107,7 +107,6 @@ using Emby.Server.Implementations.Playlists;
 using Emby.Server.Implementations;
 using Emby.Server.Implementations.ServerManager;
 using Emby.Server.Implementations.Session;
-using Emby.Server.Implementations.Social;
 using Emby.Server.Implementations.TV;
 using Emby.Server.Implementations.Updates;
 using MediaBrowser.Model.Activity;
@@ -294,6 +293,13 @@ namespace Emby.Server.Core
             ImageEncoder = imageEncoder;
 
             SetBaseExceptionMessage();
+
+            if (environmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows)
+            {
+                fileSystem.AddShortcutHandler(new LnkShortcutHandler());
+            }
+
+            fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem));
         }
 
         private Version _version;
@@ -606,7 +612,7 @@ namespace Emby.Server.Core
             CertificatePath = GetCertificatePath(true);
             Certificate = GetCertificate(CertificatePath);
 
-            HttpServer = HttpServerFactory.CreateServer(this, LogManager, ServerConfigurationManager, NetworkManager, MemoryStreamFactory, "Emby", "web/index.html", textEncoding, SocketFactory, CryptographyProvider, JsonSerializer, XmlSerializer, EnvironmentInfo, Certificate, SupportsDualModeSockets);
+            HttpServer = HttpServerFactory.CreateServer(this, LogManager, ServerConfigurationManager, NetworkManager, MemoryStreamFactory, "Emby", "web/index.html", textEncoding, SocketFactory, CryptographyProvider, JsonSerializer, XmlSerializer, EnvironmentInfo, Certificate, FileSystemManager, SupportsDualModeSockets);
             HttpServer.GlobalResponse = LocalizationManager.GetLocalizedString("StartupEmbyServerIsLoading");
             RegisterSingleInstance(HttpServer, false);
             progress.Report(10);
@@ -796,17 +802,25 @@ namespace Emby.Server.Core
                 info.FFMpegFilename = "ffmpeg";
                 info.FFProbeFilename = "ffprobe";
                 info.ArchiveType = "7z";
-                info.Version = "20160215";
+                info.Version = "20170308";
                 info.DownloadUrls = GetLinuxDownloadUrls();
             }
             else if (EnvironmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows)
             {
                 info.FFMpegFilename = "ffmpeg.exe";
                 info.FFProbeFilename = "ffprobe.exe";
-                info.Version = "20160410";
+                info.Version = "20170308";
                 info.ArchiveType = "7z";
                 info.DownloadUrls = GetWindowsDownloadUrls();
             }
+            else if (EnvironmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.OSX)
+            {
+                info.FFMpegFilename = "ffmpeg";
+                info.FFProbeFilename = "ffprobe";
+                info.ArchiveType = "7z";
+                info.Version = "20170308";
+                info.DownloadUrls = GetMacDownloadUrls();
+            }
             else
             {
                 // No version available - user requirement
@@ -816,6 +830,20 @@ namespace Emby.Server.Core
             return info;
         }
 
+        private string[] GetMacDownloadUrls()
+        {
+            switch (EnvironmentInfo.SystemArchitecture)
+            {
+                case Architecture.X64:
+                    return new[]
+                    {
+                                "https://embydata.com/downloads/ffmpeg/osx/ffmpeg-x64-20170308.7z"
+                    };
+            }
+
+            return new string[] { };
+        }
+
         private string[] GetWindowsDownloadUrls()
         {
             switch (EnvironmentInfo.SystemArchitecture)
@@ -823,12 +851,12 @@ namespace Emby.Server.Core
                 case Architecture.X64:
                     return new[]
                     {
-                                "https://github.com/MediaBrowser/Emby.Resources/raw/master/ffmpeg/windows/ffmpeg-20160410-win64.7z"
+                                "https://embydata.com/downloads/ffmpeg/windows/ffmpeg-20170308-win64.7z"
                     };
                 case Architecture.X86:
                     return new[]
                     {
-                                "https://github.com/MediaBrowser/Emby.Resources/raw/master/ffmpeg/windows/ffmpeg-20160410-win32.7z"
+                                "https://embydata.com/downloads/ffmpeg/windows/ffmpeg-20170308-win32.7z"
                     };
             }
 
@@ -842,12 +870,12 @@ namespace Emby.Server.Core
                 case Architecture.X64:
                     return new[]
                     {
-                                "https://github.com/MediaBrowser/Emby.Resources/raw/master/ffmpeg/linux/ffmpeg-git-20160215-64bit-static.7z"
+                                "https://embydata.com/downloads/ffmpeg/linux/ffmpeg-git-20170301-64bit-static.7z"
                     };
                 case Architecture.X86:
                     return new[]
                     {
-                                "https://github.com/MediaBrowser/Emby.Resources/raw/master/ffmpeg/linux/ffmpeg-git-20160215-32bit-static.7z"
+                                "https://embydata.com/downloads/ffmpeg/linux/ffmpeg-git-20170301-32bit-static.7z"
                     };
             }
 
@@ -1714,14 +1742,8 @@ namespace Emby.Server.Core
             ((IProcess)sender).Dispose();
         }
 
-        public void EnableLoopback(string appName)
-        {
-            EnableLoopbackInternal(appName);
-        }
-
-        protected virtual void EnableLoopbackInternal(string appName)
+        public virtual void EnableLoopback(string appName)
         {
-            
         }
 
         private void RegisterModules()

+ 3 - 1
Emby.Server.Core/HttpServerFactory.cs

@@ -45,6 +45,7 @@ namespace Emby.Server.Core
             IXmlSerializer xml,
             IEnvironmentInfo environment,
             ICertificate certificate,
+            IFileSystem fileSystem,
             bool enableDualModeSockets)
         {
             var logger = logManager.GetLogger("HttpServer");
@@ -65,7 +66,8 @@ namespace Emby.Server.Core
                 certificate,
                 new StreamFactory(),
                 GetParseFn,
-                enableDualModeSockets);
+                enableDualModeSockets,
+                fileSystem);
         }
 
         private static Func<string, object> GetParseFn(Type propertyType)

+ 0 - 11
Emby.Server.Core/IO/LibraryMonitor.cs

@@ -421,17 +421,6 @@ namespace Emby.Server.Core.IO
 
                 var path = e.FullPath;
 
-                // For deletes, use the parent path
-                if (e.ChangeType == WatcherChangeTypes.Deleted)
-                {
-                    var parentPath = Path.GetDirectoryName(path);
-
-                    if (!string.IsNullOrWhiteSpace(parentPath))
-                    {
-                        path = parentPath;
-                    }
-                }
-
                 ReportFileSystemChanged(path);
             }
             catch (Exception ex)

+ 28 - 4
Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs

@@ -9,6 +9,7 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using MediaBrowser.Common.Configuration;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Entities.Audio;
@@ -22,13 +23,15 @@ namespace Emby.Server.Implementations.Data
         private readonly IItemRepository _itemRepo;
         private readonly ILogger _logger;
         private readonly IFileSystem _fileSystem;
+        private readonly IApplicationPaths _appPaths;
 
-        public CleanDatabaseScheduledTask(ILibraryManager libraryManager, IItemRepository itemRepo, ILogger logger, IFileSystem fileSystem)
+        public CleanDatabaseScheduledTask(ILibraryManager libraryManager, IItemRepository itemRepo, ILogger logger, IFileSystem fileSystem, IApplicationPaths appPaths)
         {
             _libraryManager = libraryManager;
             _itemRepo = itemRepo;
             _logger = logger;
             _fileSystem = fileSystem;
+            _appPaths = appPaths;
         }
 
         public string Name
@@ -150,13 +153,27 @@ namespace Emby.Server.Implementations.Data
 
                 try
                 {
-                    if (_fileSystem.FileExists(path) || _fileSystem.DirectoryExists(path))
+                    var isPathInLibrary = false;
+
+                    if (allLibraryPaths.Any(i => path.StartsWith(i, StringComparison.Ordinal)) || 
+                        allLibraryPaths.Contains(path, StringComparer.Ordinal) || 
+                        path.StartsWith(_appPaths.ProgramDataPath, StringComparison.Ordinal))
                     {
-                        continue;
+                        isPathInLibrary = true;
+
+                        if (_fileSystem.FileExists(path) || _fileSystem.DirectoryExists(path))
+                        {
+                            continue;
+                        }
                     }
 
                     var libraryItem = _libraryManager.GetItemById(item.Item1);
 
+                    if (libraryItem == null)
+                    {
+                        continue;
+                    }
+
                     if (libraryItem.IsTopParent)
                     {
                         continue;
@@ -180,7 +197,14 @@ namespace Emby.Server.Implementations.Data
                         continue;
                     }
 
-                    _logger.Info("Deleting item from database {0} because path no longer exists. type: {1} path: {2}", libraryItem.Name, libraryItem.GetType().Name, libraryItemPath ?? string.Empty);
+                    if (isPathInLibrary)
+                    {
+                        _logger.Info("Deleting item from database {0} because path no longer exists. type: {1} path: {2}", libraryItem.Name, libraryItem.GetType().Name, libraryItemPath ?? string.Empty);
+                    }
+                    else
+                    {
+                        _logger.Info("Deleting item from database {0} because path is no longer in the server library. type: {1} path: {2}", libraryItem.Name, libraryItem.GetType().Name, libraryItemPath ?? string.Empty);
+                    }
 
                     await libraryItem.OnFileDeleted().ConfigureAwait(false);
                 }

+ 60 - 19
Emby.Server.Implementations/Data/SqliteItemRepository.cs

@@ -3204,6 +3204,40 @@ namespace Emby.Server.Implementations.Data
             }
         }
 
+        private bool IsAlphaNumeric(string str)
+        {
+            if (string.IsNullOrWhiteSpace(str))
+                return false;
+
+            for (int i = 0; i < str.Length; i++)
+            {
+                if (!(char.IsLetter(str[i])) && (!(char.IsNumber(str[i]))))
+                    return false;
+            }
+
+            return true;
+        }
+
+        private bool IsValidType(string value)
+        {
+            return IsAlphaNumeric(value);
+        }
+
+        private bool IsValidMediaType(string value)
+        {
+            return IsAlphaNumeric(value);
+        }
+
+        private bool IsValidId(string value)
+        {
+            return IsAlphaNumeric(value);
+        }
+
+        private bool IsValidPersonType(string value)
+        {
+            return IsAlphaNumeric(value);
+        }
+
         private List<string> GetWhereClauses(InternalItemsQuery query, IStatement statement, string paramSuffix = "")
         {
             if (query.IsResumable ?? false)
@@ -3423,9 +3457,9 @@ namespace Emby.Server.Implementations.Data
                     statement.TryBind("@ChannelId", query.ChannelIds[0]);
                 }
             }
-            if (query.ChannelIds.Length > 1)
+            else if (query.ChannelIds.Length > 1)
             {
-                var inClause = string.Join(",", query.ChannelIds.Select(i => "'" + i + "'").ToArray());
+                var inClause = string.Join(",", query.ChannelIds.Where(IsValidId).Select(i => "'" + i + "'").ToArray());
                 whereClauses.Add(string.Format("ChannelId in ({0})", inClause));
             }
 
@@ -4157,17 +4191,18 @@ namespace Emby.Server.Implementations.Data
                     whereClauses.Add("(IsVirtualItem=0 OR PremiereDate < DATETIME('now'))");
                 }
             }
-            if (query.MediaTypes.Length == 1)
+            var queryMediaTypes = query.MediaTypes.Where(IsValidMediaType).ToArray();
+            if (queryMediaTypes.Length == 1)
             {
                 whereClauses.Add("MediaType=@MediaTypes");
                 if (statement != null)
                 {
-                    statement.TryBind("@MediaTypes", query.MediaTypes[0]);
+                    statement.TryBind("@MediaTypes", queryMediaTypes[0]);
                 }
             }
-            if (query.MediaTypes.Length > 1)
+            else if (queryMediaTypes.Length > 1)
             {
-                var val = string.Join(",", query.MediaTypes.Select(i => "'" + i + "'").ToArray());
+                var val = string.Join(",", queryMediaTypes.Select(i => "'" + i + "'").ToArray());
 
                 whereClauses.Add("MediaType in (" + val + ")");
             }
@@ -4273,7 +4308,9 @@ namespace Emby.Server.Implementations.Data
             //var enableItemsByName = query.IncludeItemsByName ?? query.IncludeItemTypes.Length > 0;
             var enableItemsByName = query.IncludeItemsByName ?? false;
 
-            if (query.TopParentIds.Length == 1)
+            var queryTopParentIds = query.TopParentIds.Where(IsValidId).ToArray();
+
+            if (queryTopParentIds.Length == 1)
             {
                 if (enableItemsByName)
                 {
@@ -4289,12 +4326,12 @@ namespace Emby.Server.Implementations.Data
                 }
                 if (statement != null)
                 {
-                    statement.TryBind("@TopParentId", query.TopParentIds[0]);
+                    statement.TryBind("@TopParentId", queryTopParentIds[0]);
                 }
             }
-            if (query.TopParentIds.Length > 1)
+            else if (queryTopParentIds.Length > 1)
             {
-                var val = string.Join(",", query.TopParentIds.Select(i => "'" + i + "'").ToArray());
+                var val = string.Join(",", queryTopParentIds.Select(i => "'" + i + "'").ToArray());
 
                 if (enableItemsByName)
                 {
@@ -4544,7 +4581,7 @@ namespace Emby.Server.Implementations.Data
                 return result;
             }
 
-            return new[] { value };
+            return new[] { value }.Where(IsValidType);
         }
 
         public async Task DeleteItem(Guid id, CancellationToken cancellationToken)
@@ -4696,31 +4733,35 @@ namespace Emby.Server.Implementations.Data
                     statement.TryBind("@AppearsInItemId", query.AppearsInItemId.ToGuidParamValue());
                 }
             }
-            if (query.PersonTypes.Count == 1)
+            var queryPersonTypes = query.PersonTypes.Where(IsValidPersonType).ToList();
+
+            if (queryPersonTypes.Count == 1)
             {
                 whereClauses.Add("PersonType=@PersonType");
                 if (statement != null)
                 {
-                    statement.TryBind("@PersonType", query.PersonTypes[0]);
+                    statement.TryBind("@PersonType", queryPersonTypes[0]);
                 }
             }
-            if (query.PersonTypes.Count > 1)
+            else if (queryPersonTypes.Count > 1)
             {
-                var val = string.Join(",", query.PersonTypes.Select(i => "'" + i + "'").ToArray());
+                var val = string.Join(",", queryPersonTypes.Select(i => "'" + i + "'").ToArray());
 
                 whereClauses.Add("PersonType in (" + val + ")");
             }
-            if (query.ExcludePersonTypes.Count == 1)
+            var queryExcludePersonTypes = query.ExcludePersonTypes.Where(IsValidPersonType).ToList();
+
+            if (queryExcludePersonTypes.Count == 1)
             {
                 whereClauses.Add("PersonType<>@PersonType");
                 if (statement != null)
                 {
-                    statement.TryBind("@PersonType", query.ExcludePersonTypes[0]);
+                    statement.TryBind("@PersonType", queryExcludePersonTypes[0]);
                 }
             }
-            if (query.ExcludePersonTypes.Count > 1)
+            else if (queryExcludePersonTypes.Count > 1)
             {
-                var val = string.Join(",", query.ExcludePersonTypes.Select(i => "'" + i + "'").ToArray());
+                var val = string.Join(",", queryExcludePersonTypes.Select(i => "'" + i + "'").ToArray());
 
                 whereClauses.Add("PersonType not in (" + val + ")");
             }

+ 2 - 2
Emby.Server.Implementations/Dto/DtoService.cs

@@ -491,7 +491,7 @@ namespace Emby.Server.Implementations.Dto
                 }
             }
 
-            //if (!(item is LiveTvProgram) || fields.Contains(ItemFields.PlayAccess))
+            if (!(item is LiveTvProgram) || fields.Contains(ItemFields.PlayAccess))
             {
                 dto.PlayAccess = item.GetPlayAccess(user);
             }
@@ -1639,7 +1639,7 @@ namespace Emby.Server.Implementations.Dto
             var width = size.Width;
             var height = size.Height;
 
-            if (width == 0 || height == 0)
+            if (width.Equals(0) || height.Equals(0))
             {
                 return null;
             }

+ 3 - 5
Emby.Server.Implementations/Emby.Server.Implementations.csproj

@@ -84,7 +84,7 @@
     <Compile Include="FileOrganization\NameUtils.cs" />
     <Compile Include="FileOrganization\OrganizerScheduledTask.cs" />
     <Compile Include="FileOrganization\TvFolderOrganizer.cs" />
-    <Compile Include="HttpServer\GetSwaggerResource.cs" />
+    <Compile Include="HttpServer\FileWriter.cs" />
     <Compile Include="HttpServer\HttpListenerHost.cs" />
     <Compile Include="HttpServer\HttpResultFactory.cs" />
     <Compile Include="HttpServer\LoggerUtils.cs" />
@@ -102,7 +102,6 @@
     <Compile Include="HttpServer\SocketSharp\WebSocketSharpRequest.cs" />
     <Compile Include="HttpServer\SocketSharp\WebSocketSharpResponse.cs" />
     <Compile Include="HttpServer\StreamWriter.cs" />
-    <Compile Include="HttpServer\SwaggerService.cs" />
     <Compile Include="Images\BaseDynamicImageProvider.cs" />
     <Compile Include="IO\FileRefresher.cs" />
     <Compile Include="IO\MbLinkShortcutHandler.cs" />
@@ -170,7 +169,6 @@
     <Compile Include="LiveTv\RefreshChannelsScheduledTask.cs" />
     <Compile Include="LiveTv\TunerHosts\BaseTunerHost.cs" />
     <Compile Include="LiveTv\TunerHosts\HdHomerun\HdHomerunManager.cs" />
-    <Compile Include="LiveTv\TunerHosts\HdHomerun\HdHomerunDiscovery.cs" />
     <Compile Include="LiveTv\TunerHosts\HdHomerun\HdHomerunHost.cs" />
     <Compile Include="LiveTv\TunerHosts\HdHomerun\HdHomerunHttpStream.cs" />
     <Compile Include="LiveTv\TunerHosts\HdHomerun\HdHomerunUdpStream.cs" />
@@ -302,8 +300,8 @@
       <HintPath>..\packages\Emby.XmlTv.1.0.7\lib\portable-net45+win8\Emby.XmlTv.dll</HintPath>
       <Private>True</Private>
     </Reference>
-    <Reference Include="MediaBrowser.Naming, Version=1.0.6201.24431, Culture=neutral, processorArchitecture=MSIL">
-      <HintPath>..\packages\MediaBrowser.Naming.1.0.4\lib\portable-net45+win8\MediaBrowser.Naming.dll</HintPath>
+    <Reference Include="MediaBrowser.Naming, Version=1.0.6279.25941, Culture=neutral, processorArchitecture=MSIL">
+      <HintPath>..\packages\MediaBrowser.Naming.1.0.5\lib\portable-net45+win8\MediaBrowser.Naming.dll</HintPath>
       <Private>True</Private>
     </Reference>
     <Reference Include="SQLitePCL.pretty, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">

+ 8 - 38
Emby.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs

@@ -677,20 +677,7 @@ namespace Emby.Server.Implementations.FileOrganization
 
             var newPath = GetSeasonFolderPath(series, seasonNumber.Value, options);
 
-            // MAX_PATH - trailing <NULL> charachter - drive component: 260 - 1 - 3 = 256
-            // Usually newPath would include the drive component, but use 256 to be sure
-            var maxFilenameLength = 256 - newPath.Length;
-
-            if (!newPath.EndsWith(@"\"))
-            {
-                // Remove 1 for missing backslash combining path and filename
-                maxFilenameLength--;
-            }
-
-            // Remove additional 4 chars to prevent PathTooLongException for downloaded subtitles (eg. filename.ext.eng.srt)
-            maxFilenameLength -= 4;
-
-            var episodeFileName = GetEpisodeFileName(sourcePath, series.Name, seasonNumber.Value, episodeNumber.Value, endingEpisodeNumber, episodeName, options, maxFilenameLength);
+            var episodeFileName = GetEpisodeFileName(sourcePath, series.Name, seasonNumber.Value, episodeNumber.Value, endingEpisodeNumber, episodeName, options);
 
             if (string.IsNullOrEmpty(episodeFileName))
             {
@@ -742,7 +729,7 @@ namespace Emby.Server.Implementations.FileOrganization
             return Path.Combine(path, _fileSystem.GetValidFilename(seasonFolderName));
         }
 
-        private string GetEpisodeFileName(string sourcePath, string seriesName, int seasonNumber, int episodeNumber, int? endingEpisodeNumber, string episodeTitle, TvFileOrganizationOptions options, int? maxLength)
+        private string GetEpisodeFileName(string sourcePath, string seriesName, int seasonNumber, int episodeNumber, int? endingEpisodeNumber, string episodeTitle, TvFileOrganizationOptions options)
         {
             seriesName = _fileSystem.GetValidFilename(seriesName).Trim();
 
@@ -786,32 +773,15 @@ namespace Emby.Server.Implementations.FileOrganization
                 .Replace("%0e", episodeNumber.ToString("00", _usCulture))
                 .Replace("%00e", episodeNumber.ToString("000", _usCulture));
 
-            if (maxLength.HasValue && result.Contains("%#"))
-            {
-                // Substract 3 for the temp token length (%#1, %#2 or %#3)  
-                int maxRemainingTitleLength = maxLength.Value - result.Length + 3;
-                string shortenedEpisodeTitle = string.Empty;
-
-                if (maxRemainingTitleLength > 5)
-                {
-                    // A title with fewer than 5 letters wouldn't be of much value
-                    shortenedEpisodeTitle = episodeTitle.Substring(0, Math.Min(maxRemainingTitleLength, episodeTitle.Length));
-                }
-
-                result = result.Replace("%#1", shortenedEpisodeTitle)
-                    .Replace("%#2", shortenedEpisodeTitle.Replace(" ", "."))
-                    .Replace("%#3", shortenedEpisodeTitle.Replace(" ", "_"));
-            }
-
-            if (maxLength.HasValue && result.Length > maxLength.Value)
+            if (result.Contains("%#"))
             {
-                // There may be cases where reducing the title length may still not be sufficient to
-                // stay below maxLength
-                var msg = string.Format("Unable to generate an episode file name shorter than {0} characters to constrain to the max path limit", maxLength);
-                throw new Exception(msg);
+                result = result.Replace("%#1", episodeTitle)
+                    .Replace("%#2", episodeTitle.Replace(" ", "."))
+                    .Replace("%#3", episodeTitle.Replace(" ", "_"));
             }
 
-            return result;
+            // Finally, call GetValidFilename again in case user customized the episode expression with any invalid filename characters
+            return _fileSystem.GetValidFilename(result).Trim();
         }
 
         private bool IsSameEpisode(string sourcePath, string newPath)

+ 191 - 0
Emby.Server.Implementations/HttpServer/FileWriter.cs

@@ -0,0 +1,191 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Services;
+
+namespace Emby.Server.Implementations.HttpServer
+{
+    public class FileWriter : IHttpResult
+    {
+        private ILogger Logger { get; set; }
+
+        private string RangeHeader { get; set; }
+        private bool IsHeadRequest { get; set; }
+
+        private long RangeStart { get; set; }
+        private long RangeEnd { get; set; }
+        private long RangeLength { get; set; }
+        private long TotalContentLength { get; set; }
+
+        public Action OnComplete { get; set; }
+        public Action OnError { get; set; }
+        private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
+        public List<Cookie> Cookies { get; private set; }
+
+        public FileShareMode FileShare { get; set; }
+
+        /// <summary>
+        /// The _options
+        /// </summary>
+        private readonly IDictionary<string, string> _options = new Dictionary<string, string>();
+        /// <summary>
+        /// Gets the options.
+        /// </summary>
+        /// <value>The options.</value>
+        public IDictionary<string, string> Headers
+        {
+            get { return _options; }
+        }
+
+        public string Path { get; set; }
+
+        public FileWriter(string path, string contentType, string rangeHeader, ILogger logger, IFileSystem fileSystem)
+        {
+            if (string.IsNullOrEmpty(contentType))
+            {
+                throw new ArgumentNullException("contentType");
+            }
+
+            Path = path;
+            Logger = logger;
+            RangeHeader = rangeHeader;
+
+            Headers["Content-Type"] = contentType;
+
+            TotalContentLength = fileSystem.GetFileInfo(path).Length;
+
+            if (string.IsNullOrWhiteSpace(rangeHeader))
+            {
+                Headers["Content-Length"] = TotalContentLength.ToString(UsCulture);
+                StatusCode = HttpStatusCode.OK;
+            }
+            else
+            {
+                Headers["Accept-Ranges"] = "bytes";
+                StatusCode = HttpStatusCode.PartialContent;
+                SetRangeValues();
+            }
+
+            FileShare = FileShareMode.Read;
+            Cookies = new List<Cookie>();
+        }
+
+        /// <summary>
+        /// Sets the range values.
+        /// </summary>
+        private void SetRangeValues()
+        {
+            var requestedRange = RequestedRanges[0];
+
+            // If the requested range is "0-", we can optimize by just doing a stream copy
+            if (!requestedRange.Value.HasValue)
+            {
+                RangeEnd = TotalContentLength - 1;
+            }
+            else
+            {
+                RangeEnd = requestedRange.Value.Value;
+            }
+
+            RangeStart = requestedRange.Key;
+            RangeLength = 1 + RangeEnd - RangeStart;
+
+            // Content-Length is the length of what we're serving, not the original content
+            Headers["Content-Length"] = RangeLength.ToString(UsCulture);
+            Headers["Content-Range"] = string.Format("bytes {0}-{1}/{2}", RangeStart, RangeEnd, TotalContentLength);
+        }
+
+        /// <summary>
+        /// The _requested ranges
+        /// </summary>
+        private List<KeyValuePair<long, long?>> _requestedRanges;
+        /// <summary>
+        /// Gets the requested ranges.
+        /// </summary>
+        /// <value>The requested ranges.</value>
+        protected List<KeyValuePair<long, long?>> RequestedRanges
+        {
+            get
+            {
+                if (_requestedRanges == null)
+                {
+                    _requestedRanges = new List<KeyValuePair<long, long?>>();
+
+                    // Example: bytes=0-,32-63
+                    var ranges = RangeHeader.Split('=')[1].Split(',');
+
+                    foreach (var range in ranges)
+                    {
+                        var vals = range.Split('-');
+
+                        long start = 0;
+                        long? end = null;
+
+                        if (!string.IsNullOrEmpty(vals[0]))
+                        {
+                            start = long.Parse(vals[0], UsCulture);
+                        }
+                        if (!string.IsNullOrEmpty(vals[1]))
+                        {
+                            end = long.Parse(vals[1], UsCulture);
+                        }
+
+                        _requestedRanges.Add(new KeyValuePair<long, long?>(start, end));
+                    }
+                }
+
+                return _requestedRanges;
+            }
+        }
+
+        public async Task WriteToAsync(IResponse response, CancellationToken cancellationToken)
+        {
+            try
+            {
+                // Headers only
+                if (IsHeadRequest)
+                {
+                    return;
+                }
+
+                if (string.IsNullOrWhiteSpace(RangeHeader) || (RangeStart <= 0 && RangeEnd >= TotalContentLength - 1))
+                {
+                    Logger.Info("Transmit file {0}", Path);
+                    await response.TransmitFile(Path, 0, 0, FileShare, cancellationToken).ConfigureAwait(false);
+                    return;
+                }
+
+                await response.TransmitFile(Path, RangeStart, RangeLength, FileShare, cancellationToken).ConfigureAwait(false);
+            }
+            finally
+            {
+                if (OnComplete != null)
+                {
+                    OnComplete();
+                }
+            }
+        }
+
+        public string ContentType { get; set; }
+
+        public IRequest RequestContext { get; set; }
+
+        public object Response { get; set; }
+
+        public int Status { get; set; }
+
+        public HttpStatusCode StatusCode
+        {
+            get { return (HttpStatusCode)Status; }
+            set { Status = (int)value; }
+        }
+
+        public string StatusDescription { get; set; }
+
+    }
+}

+ 0 - 17
Emby.Server.Implementations/HttpServer/GetSwaggerResource.cs

@@ -1,17 +0,0 @@
-using MediaBrowser.Model.Services;
-
-namespace Emby.Server.Implementations.HttpServer
-{
-    /// <summary>
-    /// Class GetDashboardResource
-    /// </summary>
-    [Route("/swagger-ui/{ResourceName*}", "GET")]
-    public class GetSwaggerResource
-    {
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        public string ResourceName { get; set; }
-    }
-}

+ 5 - 3
Emby.Server.Implementations/HttpServer/HttpListenerHost.cs

@@ -52,6 +52,7 @@ namespace Emby.Server.Implementations.HttpServer
         private readonly ISocketFactory _socketFactory;
         private readonly ICryptoProvider _cryptoProvider;
 
+        private readonly IFileSystem _fileSystem;
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IXmlSerializer _xmlSerializer;
         private readonly ICertificate _certificate;
@@ -70,8 +71,7 @@ namespace Emby.Server.Implementations.HttpServer
             ILogger logger,
             IServerConfigurationManager config,
             string serviceName,
-            string defaultRedirectPath, INetworkManager networkManager, IMemoryStreamFactory memoryStreamProvider, ITextEncoding textEncoding, ISocketFactory socketFactory, ICryptoProvider cryptoProvider, IJsonSerializer jsonSerializer, IXmlSerializer xmlSerializer, IEnvironmentInfo environment, ICertificate certificate, IStreamFactory streamFactory, Func<Type, Func<string, object>> funcParseFn, bool enableDualModeSockets)
-            : base()
+            string defaultRedirectPath, INetworkManager networkManager, IMemoryStreamFactory memoryStreamProvider, ITextEncoding textEncoding, ISocketFactory socketFactory, ICryptoProvider cryptoProvider, IJsonSerializer jsonSerializer, IXmlSerializer xmlSerializer, IEnvironmentInfo environment, ICertificate certificate, IStreamFactory streamFactory, Func<Type, Func<string, object>> funcParseFn, bool enableDualModeSockets, IFileSystem fileSystem)
         {
             Instance = this;
 
@@ -89,6 +89,7 @@ namespace Emby.Server.Implementations.HttpServer
             _streamFactory = streamFactory;
             _funcParseFn = funcParseFn;
             _enableDualModeSockets = enableDualModeSockets;
+            _fileSystem = fileSystem;
             _config = config;
 
             _logger = logger;
@@ -226,7 +227,8 @@ namespace Emby.Server.Implementations.HttpServer
                 _cryptoProvider,
                 _streamFactory,
                 _enableDualModeSockets,
-                GetRequest);
+                GetRequest,
+                _fileSystem);
         }
 
         private IHttpRequest GetRequest(HttpListenerContext httpContext)

+ 26 - 16
Emby.Server.Implementations/HttpServer/HttpResultFactory.cs

@@ -474,10 +474,6 @@ namespace Emby.Server.Implementations.HttpServer
             {
                 throw new ArgumentNullException("cacheKey");
             }
-            if (options.ContentFactory == null)
-            {
-                throw new ArgumentNullException("factoryFn");
-            }
 
             var key = cacheKey.ToString("N");
 
@@ -560,30 +556,44 @@ namespace Emby.Server.Implementations.HttpServer
             {
                 var rangeHeader = requestContext.Headers.Get("Range");
 
-                var stream = await factoryFn().ConfigureAwait(false);
+                if (!isHeadRequest && !string.IsNullOrWhiteSpace(options.Path))
+                {
+                    return new FileWriter(options.Path, contentType, rangeHeader, _logger, _fileSystem)
+                    {
+                        OnComplete = options.OnComplete,
+                        OnError = options.OnError,
+                        FileShare = options.FileShare
+                    };
+                }
 
                 if (!string.IsNullOrEmpty(rangeHeader))
                 {
+                    var stream = await factoryFn().ConfigureAwait(false);
+
                     return new RangeRequestWriter(rangeHeader, stream, contentType, isHeadRequest, _logger)
                     {
                         OnComplete = options.OnComplete
                     };
                 }
+                else
+                {
+                    var stream = await factoryFn().ConfigureAwait(false);
 
-                responseHeaders["Content-Length"] = stream.Length.ToString(UsCulture);
+                    responseHeaders["Content-Length"] = stream.Length.ToString(UsCulture);
 
-                if (isHeadRequest)
-                {
-                    stream.Dispose();
+                    if (isHeadRequest)
+                    {
+                        stream.Dispose();
 
-                    return GetHttpResult(new byte[] { }, contentType, true);
-                }
+                        return GetHttpResult(new byte[] { }, contentType, true);
+                    }
 
-                return new StreamWriter(stream, contentType, _logger)
-                {
-                    OnComplete = options.OnComplete,
-                    OnError = options.OnError
-                };
+                    return new StreamWriter(stream, contentType, _logger)
+                    {
+                        OnComplete = options.OnComplete,
+                        OnError = options.OnError
+                    };
+                }
             }
 
             using (var stream = await factoryFn().ConfigureAwait(false))

+ 4 - 2
Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs

@@ -27,10 +27,11 @@ namespace Emby.Server.Implementations.HttpServer.SocketSharp
         private readonly ISocketFactory _socketFactory;
         private readonly ICryptoProvider _cryptoProvider;
         private readonly IStreamFactory _streamFactory;
+        private readonly IFileSystem _fileSystem;
         private readonly Func<HttpListenerContext, IHttpRequest> _httpRequestFactory;
         private readonly bool _enableDualMode;
 
-        public WebSocketSharpListener(ILogger logger, ICertificate certificate, IMemoryStreamFactory memoryStreamProvider, ITextEncoding textEncoding, INetworkManager networkManager, ISocketFactory socketFactory, ICryptoProvider cryptoProvider, IStreamFactory streamFactory, bool enableDualMode, Func<HttpListenerContext, IHttpRequest> httpRequestFactory)
+        public WebSocketSharpListener(ILogger logger, ICertificate certificate, IMemoryStreamFactory memoryStreamProvider, ITextEncoding textEncoding, INetworkManager networkManager, ISocketFactory socketFactory, ICryptoProvider cryptoProvider, IStreamFactory streamFactory, bool enableDualMode, Func<HttpListenerContext, IHttpRequest> httpRequestFactory, IFileSystem fileSystem)
         {
             _logger = logger;
             _certificate = certificate;
@@ -42,6 +43,7 @@ namespace Emby.Server.Implementations.HttpServer.SocketSharp
             _streamFactory = streamFactory;
             _enableDualMode = enableDualMode;
             _httpRequestFactory = httpRequestFactory;
+            _fileSystem = fileSystem;
         }
 
         public Action<Exception, IRequest, bool> ErrorHandler { get; set; }
@@ -54,7 +56,7 @@ namespace Emby.Server.Implementations.HttpServer.SocketSharp
         public void Start(IEnumerable<string> urlPrefixes)
         {
             if (_listener == null)
-                _listener = new HttpListener(_logger, _cryptoProvider, _streamFactory, _socketFactory, _networkManager, _textEncoding, _memoryStreamProvider);
+                _listener = new HttpListener(_logger, _cryptoProvider, _streamFactory, _socketFactory, _networkManager, _textEncoding, _memoryStreamProvider, _fileSystem);
 
             _listener.EnableDualMode = _enableDualMode;
 

+ 8 - 0
Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs

@@ -3,6 +3,9 @@ using System.Collections.Generic;
 using System.IO;
 using System.Net;
 using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Logging;
 using SocketHttpListener.Net;
 using HttpListenerResponse = SocketHttpListener.Net.HttpListenerResponse;
@@ -189,5 +192,10 @@ namespace Emby.Server.Implementations.HttpServer.SocketSharp
         public void ClearCookies()
         {
         }
+
+        public Task TransmitFile(string path, long offset, long count, FileShareMode fileShareMode, CancellationToken cancellationToken)
+        {
+            return _response.TransmitFile(path, offset, count, fileShareMode, cancellationToken);
+        }
     }
 }

+ 0 - 47
Emby.Server.Implementations/HttpServer/SwaggerService.cs

@@ -1,47 +0,0 @@
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Net;
-using System.IO;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Services;
-
-namespace Emby.Server.Implementations.HttpServer
-{
-    public class SwaggerService : IService, IRequiresRequest
-    {
-        private readonly IServerApplicationPaths _appPaths;
-        private readonly IFileSystem _fileSystem;
-
-        public SwaggerService(IServerApplicationPaths appPaths, IFileSystem fileSystem, IHttpResultFactory resultFactory)
-        {
-            _appPaths = appPaths;
-            _fileSystem = fileSystem;
-            _resultFactory = resultFactory;
-        }
-
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetSwaggerResource request)
-        {
-            var swaggerDirectory = Path.Combine(_appPaths.ApplicationResourcesPath, "swagger-ui");
-
-            var requestedFile = Path.Combine(swaggerDirectory, request.ResourceName.Replace('/', _fileSystem.DirectorySeparatorChar));
-
-            return _resultFactory.GetStaticFileResult(Request, requestedFile).Result;
-        }
-
-        /// <summary>
-        /// Gets or sets the result factory.
-        /// </summary>
-        /// <value>The result factory.</value>
-        private readonly IHttpResultFactory _resultFactory;
-
-        /// <summary>
-        /// Gets or sets the request context.
-        /// </summary>
-        /// <value>The request context.</value>
-        public IRequest Request { get; set; }
-    }
-}

+ 1 - 1
Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs

@@ -258,7 +258,7 @@ namespace Emby.Server.Implementations.Images
                 {
                     return await CreateSquareCollage(item, itemsWithImages, outputPath).ConfigureAwait(false);
                 }
-                if (item is Playlist || item is MusicGenre)
+                if (item is Playlist || item is MusicGenre || item is Genre || item is GameGenre)
                 {
                     return await CreateSquareCollage(item, itemsWithImages, outputPath).ConfigureAwait(false);
                 }

+ 17 - 10
Emby.Server.Implementations/Library/LibraryManager.cs

@@ -513,6 +513,11 @@ namespace Emby.Server.Implementations.Library
         }
 
         public Guid GetNewItemId(string key, Type type)
+        {
+            return GetNewItemIdInternal(key, type, false);
+        }
+
+        private Guid GetNewItemIdInternal(string key, Type type, bool forceCaseInsensitive)
         {
             if (string.IsNullOrWhiteSpace(key))
             {
@@ -531,7 +536,7 @@ namespace Emby.Server.Implementations.Library
                     .Replace("/", "\\");
             }
 
-            if (!ConfigurationManager.Configuration.EnableCaseSensitiveItemIds)
+            if (forceCaseInsensitive || !ConfigurationManager.Configuration.EnableCaseSensitiveItemIds)
             {
                 key = key.ToLower();
             }
@@ -865,7 +870,7 @@ namespace Emby.Server.Implementations.Library
         /// <returns>Task{Person}.</returns>
         public Person GetPerson(string name)
         {
-            return CreateItemByName<Person>(Person.GetPath(name), name);
+            return CreateItemByName<Person>(Person.GetPath, name);
         }
 
         /// <summary>
@@ -875,7 +880,7 @@ namespace Emby.Server.Implementations.Library
         /// <returns>Task{Studio}.</returns>
         public Studio GetStudio(string name)
         {
-            return CreateItemByName<Studio>(Studio.GetPath(name), name);
+            return CreateItemByName<Studio>(Studio.GetPath, name);
         }
 
         /// <summary>
@@ -885,7 +890,7 @@ namespace Emby.Server.Implementations.Library
         /// <returns>Task{Genre}.</returns>
         public Genre GetGenre(string name)
         {
-            return CreateItemByName<Genre>(Genre.GetPath(name), name);
+            return CreateItemByName<Genre>(Genre.GetPath, name);
         }
 
         /// <summary>
@@ -895,7 +900,7 @@ namespace Emby.Server.Implementations.Library
         /// <returns>Task{MusicGenre}.</returns>
         public MusicGenre GetMusicGenre(string name)
         {
-            return CreateItemByName<MusicGenre>(MusicGenre.GetPath(name), name);
+            return CreateItemByName<MusicGenre>(MusicGenre.GetPath, name);
         }
 
         /// <summary>
@@ -905,7 +910,7 @@ namespace Emby.Server.Implementations.Library
         /// <returns>Task{GameGenre}.</returns>
         public GameGenre GetGameGenre(string name)
         {
-            return CreateItemByName<GameGenre>(GameGenre.GetPath(name), name);
+            return CreateItemByName<GameGenre>(GameGenre.GetPath, name);
         }
 
         /// <summary>
@@ -923,7 +928,7 @@ namespace Emby.Server.Implementations.Library
 
             var name = value.ToString(CultureInfo.InvariantCulture);
 
-            return CreateItemByName<Year>(Year.GetPath(name), name);
+            return CreateItemByName<Year>(Year.GetPath, name);
         }
 
         /// <summary>
@@ -933,10 +938,10 @@ namespace Emby.Server.Implementations.Library
         /// <returns>Task{Genre}.</returns>
         public MusicArtist GetArtist(string name)
         {
-            return CreateItemByName<MusicArtist>(MusicArtist.GetPath(name), name);
+            return CreateItemByName<MusicArtist>(MusicArtist.GetPath, name);
         }
 
-        private T CreateItemByName<T>(string path, string name)
+        private T CreateItemByName<T>(Func<string,string> getPathFn, string name)
             where T : BaseItem, new()
         {
             if (typeof(T) == typeof(MusicArtist))
@@ -957,7 +962,9 @@ namespace Emby.Server.Implementations.Library
                 }
             }
 
-            var id = GetNewItemId(path, typeof(T));
+            var path = getPathFn(name);
+            var forceCaseInsensitiveId = ConfigurationManager.Configuration.EnableNormalizedItemByNameIds;
+            var id = GetNewItemIdInternal(path, typeof(T), forceCaseInsensitiveId);
 
             var item = GetItemById(id) as T;
 

+ 2 - 0
Emby.Server.Implementations/Library/MediaSourceManager.cs

@@ -369,6 +369,8 @@ namespace Emby.Server.Implementations.Library
         {
             await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
 
+            enableAutoClose = false;
+
             try
             {
                 var tuple = GetProvider(request.OpenToken);

+ 146 - 7
Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs

@@ -442,6 +442,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             {
                 result = await provider.GetChannels(info, cancellationToken).ConfigureAwait(false);
 
+                foreach (var channel in result)
+                {
+                    _logger.Info("Found epg channel in {0} {1} {2} {3}", provider.Name, info.ListingsId, channel.Name, channel.Id);
+                }
+
                 _epgChannels.AddOrUpdate(info.Id, result, (k, v) => result);
             }
 
@@ -493,11 +498,17 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
             if (!string.IsNullOrWhiteSpace(tunerChannel.TunerChannelId))
             {
-                var mappedTunerChannelId = GetMappedChannel(tunerChannel.TunerChannelId, mappings);
+                var tunerChannelId = tunerChannel.TunerChannelId;
+                if (tunerChannelId.IndexOf(".json.schedulesdirect.org", StringComparison.OrdinalIgnoreCase) != -1)
+                {
+                    tunerChannelId = tunerChannelId.Replace(".json.schedulesdirect.org", string.Empty, StringComparison.OrdinalIgnoreCase).TrimStart('I');
+                }
+
+                var mappedTunerChannelId = GetMappedChannel(tunerChannelId, mappings);
 
                 if (string.IsNullOrWhiteSpace(mappedTunerChannelId))
                 {
-                    mappedTunerChannelId = tunerChannel.TunerChannelId;
+                    mappedTunerChannelId = tunerChannelId;
                 }
 
                 var channel = epgChannels.FirstOrDefault(i => string.Equals(mappedTunerChannelId, i.Id, StringComparison.OrdinalIgnoreCase));
@@ -639,8 +650,9 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
         public Task<string> CreateTimer(TimerInfo timer, CancellationToken cancellationToken)
         {
-            var existingTimer = _timerProvider.GetAll()
-                .FirstOrDefault(i => string.Equals(timer.ProgramId, i.ProgramId, StringComparison.OrdinalIgnoreCase));
+            var existingTimer = string.IsNullOrWhiteSpace(timer.ProgramId) ?
+                null :
+                _timerProvider.GetTimerByProgramId(timer.ProgramId);
 
             if (existingTimer != null)
             {
@@ -710,7 +722,34 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                 throw new InvalidOperationException("SeriesId for program not found");
             }
 
+            // If any timers have already been manually created, make sure they don't get cancelled
+            var existingTimers = (await GetTimersAsync(CancellationToken.None).ConfigureAwait(false))
+                .Where(i =>
+                {
+                    if (string.Equals(i.ProgramId, info.ProgramId, StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(info.ProgramId))
+                    {
+                        return true;
+                    }
+
+                    if (string.Equals(i.SeriesId, info.SeriesId, StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(info.SeriesId))
+                    {
+                        return true;
+                    }
+
+                    return false;
+                })
+                .ToList();
+
             _seriesTimerProvider.Add(info);
+
+            foreach (var timer in existingTimers)
+            {
+                timer.SeriesTimerId = info.Id;
+                timer.IsManual = true;
+
+                _timerProvider.AddOrUpdate(timer, false);
+            }
+
             await UpdateTimersForSeriesTimer(epgData, info, true, false).ConfigureAwait(false);
 
             return info.Id;
@@ -991,6 +1030,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
                 if (epgChannel == null)
                 {
+                    _logger.Debug("EPG channel not found for tuner channel {0}-{1} from {2}-{3}", channel.Number, channel.Name, provider.Item1.Name, provider.Item2.ListingsId ?? string.Empty);
                     programs = new List<ProgramInfo>();
                 }
                 else
@@ -1276,6 +1316,14 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                     return;
                 }
 
+                var registration = await _liveTvManager.GetRegistrationInfo("dvr").ConfigureAwait(false);
+                if (!registration.IsValid)
+                {
+                    _logger.Warn("Emby Premiere required to use Emby DVR.");
+                    OnTimerOutOfDate(timer);
+                    return;
+                }
+
                 var activeRecordingInfo = new ActiveRecordingInfo
                 {
                     CancellationTokenSource = new CancellationTokenSource(),
@@ -2299,6 +2347,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                 {
                     var existingTimer = _timerProvider.GetTimer(timer.Id);
 
+                    if (existingTimer == null)
+                    {
+                        existingTimer = string.IsNullOrWhiteSpace(timer.ProgramId)
+                            ? null
+                            : _timerProvider.GetTimerByProgramId(timer.ProgramId);
+                    }
+
                     if (existingTimer == null)
                     {
                         if (ShouldCancelTimerForSeriesTimer(seriesTimer, timer))
@@ -2313,12 +2368,16 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                     }
                     else
                     {
-                        // Only update if not currently active
+                        // Only update if not currently active - test both new timer and existing in case Id's are different
+                        // Id's could be different if the timer was created manually prior to series timer creation
                         ActiveRecordingInfo activeRecordingInfo;
-                        if (!_activeRecordings.TryGetValue(timer.Id, out activeRecordingInfo))
+                        if (!_activeRecordings.TryGetValue(timer.Id, out activeRecordingInfo) && !_activeRecordings.TryGetValue(existingTimer.Id, out activeRecordingInfo))
                         {
                             UpdateExistingTimerWithNewMetadata(existingTimer, timer);
 
+                            // Needed by ShouldCancelTimerForSeriesTimer
+                            timer.IsManual = existingTimer.IsManual;
+
                             if (ShouldCancelTimerForSeriesTimer(seriesTimer, timer))
                             {
                                 existingTimer.Status = RecordingStatus.Cancelled;
@@ -2516,7 +2575,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                 list.Add(new VirtualFolderInfo
                 {
                     Locations = new List<string> { customPath },
-                    Name = "Recorded Series",
+                    Name = "Recorded Shows",
                     CollectionType = CollectionType.TvShows
                 });
             }
@@ -2531,6 +2590,86 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             public ProgramInfo Program { get; set; }
             public CancellationTokenSource CancellationTokenSource { get; set; }
         }
+
+        private const int TunerDiscoveryDurationMs = 3000;
+
+        public async Task<List<TunerHostInfo>> DiscoverTuners(bool newDevicesOnly, CancellationToken cancellationToken)
+        {
+            var list = new List<TunerHostInfo>();
+
+            var configuredDeviceIds = GetConfiguration().TunerHosts
+               .Where(i => !string.IsNullOrWhiteSpace(i.DeviceId))
+               .Select(i => i.DeviceId)
+               .ToList();
+
+            foreach (var host in _liveTvManager.TunerHosts)
+            {
+                var discoveredDevices = await DiscoverDevices(host, TunerDiscoveryDurationMs, cancellationToken).ConfigureAwait(false);
+
+                if (newDevicesOnly)
+                {
+                    discoveredDevices = discoveredDevices.Where(d => !configuredDeviceIds.Contains(d.DeviceId, StringComparer.OrdinalIgnoreCase))
+                            .ToList();
+                }
+                list.AddRange(discoveredDevices);
+            }
+
+            return list;
+        }
+
+        public async Task ScanForTunerDeviceChanges(CancellationToken cancellationToken)
+        {
+            foreach (var host in _liveTvManager.TunerHosts)
+            {
+                await ScanForTunerDeviceChanges(host, cancellationToken).ConfigureAwait(false);
+            }
+        }
+
+        private async Task ScanForTunerDeviceChanges(ITunerHost host, CancellationToken cancellationToken)
+        {
+            var discoveredDevices = await DiscoverDevices(host, TunerDiscoveryDurationMs, cancellationToken).ConfigureAwait(false);
+
+            var configuredDevices = GetConfiguration().TunerHosts
+                .Where(i => string.Equals(i.Type, host.Type, StringComparison.OrdinalIgnoreCase))
+                .ToList();
+
+            foreach (var device in discoveredDevices)
+            {
+                var configuredDevice = configuredDevices.FirstOrDefault(i => string.Equals(i.DeviceId, device.DeviceId, StringComparison.OrdinalIgnoreCase));
+
+                if (configuredDevice != null)
+                {
+                    if (!string.Equals(device.Url, configuredDevice.Url, StringComparison.OrdinalIgnoreCase))
+                    {
+                        _logger.Info("Tuner url has changed from {0} to {1}", configuredDevice.Url, device.Url);
+
+                        configuredDevice.Url = device.Url;
+                        await _liveTvManager.SaveTunerHost(configuredDevice).ConfigureAwait(false);
+                    }
+                }
+            }
+        }
+
+        private async Task<List<TunerHostInfo>> DiscoverDevices(ITunerHost host, int discoveryDuationMs, CancellationToken cancellationToken)
+        {
+            try
+            {
+                var discoveredDevices = await host.DiscoverDevices(discoveryDuationMs, cancellationToken).ConfigureAwait(false);
+
+                foreach (var device in discoveredDevices)
+                {
+                    _logger.Info("Discovered tuner device {0} at {1}", host.Name, device.Url);
+                }
+
+                return discoveredDevices;
+            }
+            catch (Exception ex)
+            {
+                _logger.ErrorException("Error discovering tuner devices", ex);
+
+                return new List<TunerHostInfo>();
+            }
+        }
     }
     public static class ConfigurationExtension
     {

+ 6 - 2
Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs

@@ -163,7 +163,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
             var durationParam = " -t " + _mediaEncoder.GetTimeParameter(duration.Ticks);
             var inputModifiers = "-fflags +genpts -async 1 -vsync -1";
-            var commandLineArgs = "-i \"{0}\"{5} {2} -map_metadata -1 -threads 0 {3}{4} -y \"{1}\"";
+            var commandLineArgs = "-i \"{0}\"{5} {2} -map_metadata -1 -threads 0 {3}{4}{6} -y \"{1}\"";
 
             long startTimeTicks = 0;
             //if (mediaSource.DateLiveStreamOpened.HasValue)
@@ -193,7 +193,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
             var subtitleArgs = CopySubtitles ? " -codec:s copy" : " -sn";
 
-            commandLineArgs = string.Format(commandLineArgs, inputTempFile, targetFile, videoArgs, GetAudioArgs(mediaSource), subtitleArgs, durationParam);
+            var outputParam = string.Equals(Path.GetExtension(targetFile), ".mp4", StringComparison.OrdinalIgnoreCase) ?
+                " -f mp4 -movflags frag_keyframe+empty_moov" :
+                string.Empty;
+
+            commandLineArgs = string.Format(commandLineArgs, inputTempFile, targetFile, videoArgs, GetAudioArgs(mediaSource), subtitleArgs, durationParam, outputParam);
 
             return inputModifiers + " " + commandLineArgs;
         }

+ 5 - 0
Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs

@@ -166,5 +166,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
         {
             return GetAll().FirstOrDefault(r => string.Equals(r.Id, id, StringComparison.OrdinalIgnoreCase));
         }
+
+        public TimerInfo GetTimerByProgramId(string programId)
+        {
+            return GetAll().FirstOrDefault(r => string.Equals(r.ProgramId, programId, StringComparison.OrdinalIgnoreCase));
+        }
     }
 }

+ 9 - 0
Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs

@@ -205,6 +205,15 @@ namespace Emby.Server.Implementations.LiveTv.Listings
                 }
 
                 programInfo.ShowId = uniqueString.GetMD5().ToString("N");
+
+                // If we don't have valid episode info, assume it's a unique program, otherwise recordings might be skipped
+                if (programInfo.IsSeries && !programInfo.IsRepeat)
+                {
+                    if ((programInfo.EpisodeNumber ?? 0) == 0)
+                    {
+                        programInfo.ShowId = programInfo.ShowId + programInfo.StartDate.Ticks.ToString(CultureInfo.InvariantCulture);
+                    }
+                }
             }
 
             // Construct an id from the channel and start date

+ 38 - 62
Emby.Server.Implementations/LiveTv/LiveTvManager.cs

@@ -150,6 +150,21 @@ namespace Emby.Server.Implementations.LiveTv
             get { return _listingProviders; }
         }
 
+        public List<NameIdPair> GetTunerHostTypes()
+        {
+            return _tunerHosts.OrderBy(i => i.Name).Select(i => new NameIdPair
+            {
+                Name = i.Name,
+                Id = i.Type
+
+            }).ToList();
+        }
+
+        public Task<List<TunerHostInfo>> DiscoverTuners(bool newDevicesOnly, CancellationToken cancellationToken)
+        {
+            return EmbyTV.EmbyTV.Current.DiscoverTuners(newDevicesOnly, cancellationToken);
+        }
+
         void service_DataSourceChanged(object sender, EventArgs e)
         {
             if (!_isDisposed)
@@ -1063,25 +1078,28 @@ namespace Emby.Server.Implementations.LiveTv
 
             var channel = GetInternalChannel(program.ChannelId);
 
-            var channelUserdata = _userDataManager.GetUserData(userId, channel);
-
-            if (channelUserdata.Likes ?? false)
-            {
-                score += 2;
-            }
-            else if (!(channelUserdata.Likes ?? true))
+            if (channel != null)
             {
-                score -= 2;
-            }
+                var channelUserdata = _userDataManager.GetUserData(userId, channel);
 
-            if (channelUserdata.IsFavorite)
-            {
-                score += 3;
-            }
+                if (channelUserdata.Likes ?? false)
+                {
+                    score += 2;
+                }
+                else if (!(channelUserdata.Likes ?? true))
+                {
+                    score -= 2;
+                }
 
-            if (factorChannelWatchCount)
-            {
-                score += channelUserdata.PlayCount;
+                if (channelUserdata.IsFavorite)
+                {
+                    score += 3;
+                }
+
+                if (factorChannelWatchCount)
+                {
+                    score += channelUserdata.PlayCount;
+                }
             }
 
             return score;
@@ -1180,6 +1198,8 @@ namespace Emby.Server.Implementations.LiveTv
         {
             EmbyTV.EmbyTV.Current.CreateRecordingFolders();
 
+            await EmbyTV.EmbyTV.Current.ScanForTunerDeviceChanges(cancellationToken).ConfigureAwait(false);
+
             var numComplete = 0;
             double progressPerService = _services.Count == 0
                 ? 0
@@ -2748,7 +2768,7 @@ namespace Emby.Server.Implementations.LiveTv
 
         private bool IsLiveTvEnabled(User user)
         {
-            return user.Policy.EnableLiveTvAccess && (Services.Count > 1 || GetConfiguration().TunerHosts.Count(i => i.IsEnabled) > 0);
+            return user.Policy.EnableLiveTvAccess && (Services.Count > 1 || GetConfiguration().TunerHosts.Count > 0);
         }
 
         public IEnumerable<User> GetEnabledUsers()
@@ -2986,7 +3006,7 @@ namespace Emby.Server.Implementations.LiveTv
             if (string.Equals(feature, "dvr-l", StringComparison.OrdinalIgnoreCase))
             {
                 var config = GetConfiguration();
-                if (config.TunerHosts.Count(i => i.IsEnabled) > 0 &&
+                if (config.TunerHosts.Count > 0 &&
                     config.ListingProviders.Count(i => (i.EnableAllTuners || i.EnabledTuners.Length > 0) && string.Equals(i.Type, SchedulesDirect.TypeName, StringComparison.OrdinalIgnoreCase)) > 0)
                 {
                     return Task.FromResult(new MBRegistrationRecord
@@ -3000,50 +3020,6 @@ namespace Emby.Server.Implementations.LiveTv
             return _security.GetRegistrationStatus(feature);
         }
 
-        public List<NameValuePair> GetSatIniMappings()
-        {
-            return new List<NameValuePair>();
-            //var names = GetType().Assembly.GetManifestResourceNames().Where(i => i.IndexOf("SatIp.ini", StringComparison.OrdinalIgnoreCase) != -1).ToList();
-
-            //return names.Select(GetSatIniMappings).Where(i => i != null).DistinctBy(i => i.Value.Split('|')[0]).ToList();
-        }
-
-        public NameValuePair GetSatIniMappings(string resource)
-        {
-            return new NameValuePair();
-            //using (var stream = GetType().Assembly.GetManifestResourceStream(resource))
-            //{
-            //    using (var reader = new StreamReader(stream))
-            //    {
-            //        var parser = new StreamIniDataParser();
-            //        IniData data = parser.ReadData(reader);
-
-            //        var satType1 = data["SATTYPE"]["1"];
-            //        var satType2 = data["SATTYPE"]["2"];
-
-            //        if (string.IsNullOrWhiteSpace(satType2))
-            //        {
-            //            return null;
-            //        }
-
-            //        var srch = "SatIp.ini.";
-            //        var filename = Path.GetFileName(resource);
-
-            //        return new NameValuePair
-            //        {
-            //            Name = satType1 + " " + satType2,
-            //            Value = satType2 + "|" + filename.Substring(filename.IndexOf(srch) + srch.Length)
-            //        };
-            //    }
-            //}
-        }
-
-        public Task<List<ChannelInfo>> GetSatChannelScanResult(TunerHostInfo info, CancellationToken cancellationToken)
-        {
-            return Task.FromResult(new List<ChannelInfo>());
-            //return new TunerHosts.SatIp.ChannelScan(_logger).Scan(info, cancellationToken);
-        }
-
         public Task<List<ChannelInfo>> GetChannelsForListingsProvider(string id, CancellationToken cancellationToken)
         {
             var info = GetConfiguration().ListingProviders.First(i => string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase));

+ 1 - 1
Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs

@@ -62,7 +62,7 @@ namespace Emby.Server.Implementations.LiveTv
 
         public bool IsHidden
         {
-            get { return _liveTvManager.Services.Count == 1 && GetConfiguration().TunerHosts.Count(i => i.IsEnabled) == 0; }
+            get { return _liveTvManager.Services.Count == 1 && GetConfiguration().TunerHosts.Count == 0; }
         }
 
         public bool IsEnabled

+ 24 - 5
Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs

@@ -53,7 +53,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
 
             var result = await GetChannelsInternal(tuner, cancellationToken).ConfigureAwait(false);
             var list = result.ToList();
-            Logger.Debug("Channels from {0}: {1}", tuner.Url, JsonSerializer.SerializeToString(list));
+            Logger.Info("Channels from {0}: {1}", tuner.Url, JsonSerializer.SerializeToString(list));
 
             if (!string.IsNullOrWhiteSpace(key) && list.Count > 0)
             {
@@ -69,7 +69,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
         protected virtual List<TunerHostInfo> GetTunerHosts()
         {
             return GetConfiguration().TunerHosts
-                .Where(i => i.IsEnabled && string.Equals(i.Type, Type, StringComparison.OrdinalIgnoreCase))
+                .Where(i => string.Equals(i.Type, Type, StringComparison.OrdinalIgnoreCase))
                 .ToList();
         }
 
@@ -135,8 +135,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
                     {
                         // Check to make sure the tuner is available
                         // If there's only one tuner, don't bother with the check and just let the tuner be the one to throw an error
-                        if (hostsWithChannel.Count > 1 &&
-                            !await IsAvailable(host, channelId, cancellationToken).ConfigureAwait(false))
+                        if (hostsWithChannel.Count > 1 && !await IsAvailable(host, channelId, cancellationToken).ConfigureAwait(false))
                         {
                             Logger.Error("Tuner is not currently available");
                             continue;
@@ -208,6 +207,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
 
             foreach (var host in hostsWithChannel)
             {
+                if (!channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase))
+                {
+                    continue;
+                }
+
                 try
                 {
                     var liveStream = await GetChannelStream(host, channelId, streamId, cancellationToken).ConfigureAwait(false);
@@ -243,7 +247,22 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
 
         protected abstract Task<bool> IsAvailableInternal(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken);
 
-        protected abstract bool IsValidChannelId(string channelId);
+        protected virtual string ChannelIdPrefix
+        {
+            get
+            {
+                return Type + "_";
+            }
+        }
+        protected virtual bool IsValidChannelId(string channelId)
+        {
+            if (string.IsNullOrWhiteSpace(channelId))
+            {
+                throw new ArgumentNullException("channelId");
+            }
+
+            return channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase);
+        }
 
         protected LiveTvOptions GetConfiguration()
         {

+ 0 - 158
Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunDiscovery.cs

@@ -1,158 +0,0 @@
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dlna;
-using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Controller.Plugins;
-using MediaBrowser.Model.Extensions;
-using MediaBrowser.Model.LiveTv;
-using MediaBrowser.Model.Logging;
-using System;
-using System.Linq;
-using System.Threading;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.Events;
-using MediaBrowser.Model.Serialization;
-
-namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
-{
-    public class HdHomerunDiscovery : IServerEntryPoint
-    {
-        private readonly IDeviceDiscovery _deviceDiscovery;
-        private readonly IServerConfigurationManager _config;
-        private readonly ILogger _logger;
-        private readonly ILiveTvManager _liveTvManager;
-        private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
-        private readonly IHttpClient _httpClient;
-        private readonly IJsonSerializer _json;
-
-        public HdHomerunDiscovery(IDeviceDiscovery deviceDiscovery, IServerConfigurationManager config, ILogger logger, ILiveTvManager liveTvManager, IHttpClient httpClient, IJsonSerializer json)
-        {
-            _deviceDiscovery = deviceDiscovery;
-            _config = config;
-            _logger = logger;
-            _liveTvManager = liveTvManager;
-            _httpClient = httpClient;
-            _json = json;
-        }
-
-        public void Run()
-        {
-            _deviceDiscovery.DeviceDiscovered += _deviceDiscovery_DeviceDiscovered;
-        }
-
-        void _deviceDiscovery_DeviceDiscovered(object sender, GenericEventArgs<UpnpDeviceInfo> e)
-        {
-            string server = null;
-            var info = e.Argument;
-
-            if (info.Headers.TryGetValue("SERVER", out server) && server.IndexOf("HDHomeRun", StringComparison.OrdinalIgnoreCase) != -1)
-            {
-                string location;
-                if (info.Headers.TryGetValue("Location", out location))
-                {
-                    //_logger.Debug("HdHomerun found at {0}", location);
-
-                    // Just get the beginning of the url
-                    Uri uri;
-                    if (Uri.TryCreate(location, UriKind.Absolute, out uri))
-                    {
-                        var apiUrl = location.Replace(uri.LocalPath, String.Empty, StringComparison.OrdinalIgnoreCase)
-                                .TrimEnd('/');
-
-                        //_logger.Debug("HdHomerun api url: {0}", apiUrl);
-                        AddDevice(apiUrl);
-                    }
-                }
-            }
-        }
-
-        private async void AddDevice(string url)
-        {
-            await _semaphore.WaitAsync().ConfigureAwait(false);
-
-            try
-            {
-                var options = GetConfiguration();
-
-                if (options.TunerHosts.Any(i =>
-                            string.Equals(i.Type, HdHomerunHost.DeviceType, StringComparison.OrdinalIgnoreCase) &&
-                            UriEquals(i.Url, url)))
-                {
-                    return;
-                }
-
-                // Strip off the port
-                url = new Uri(url).GetComponents(UriComponents.AbsoluteUri & ~UriComponents.Port, UriFormat.UriEscaped).TrimEnd('/');
-
-                // Test it by pulling down the lineup
-                using (var stream = await _httpClient.Get(new HttpRequestOptions
-                {
-                    Url = string.Format("{0}/discover.json", url),
-                    CancellationToken = CancellationToken.None,
-                    BufferContent = false
-                }))
-                {
-                    var response = _json.DeserializeFromStream<HdHomerunHost.DiscoverResponse>(stream);
-
-                    var existing = GetConfiguration().TunerHosts
-                        .FirstOrDefault(i => string.Equals(i.Type, HdHomerunHost.DeviceType, StringComparison.OrdinalIgnoreCase) && string.Equals(i.DeviceId, response.DeviceID, StringComparison.OrdinalIgnoreCase));
-
-                    if (existing == null)
-                    {
-                        await _liveTvManager.SaveTunerHost(new TunerHostInfo
-                        {
-                            Type = HdHomerunHost.DeviceType,
-                            Url = url,
-                            DeviceId = response.DeviceID
-
-                        }).ConfigureAwait(false);
-                    }
-                    else
-                    {
-                        if (!string.Equals(existing.Url, url, StringComparison.OrdinalIgnoreCase))
-                        {
-                            existing.Url = url;
-                            await _liveTvManager.SaveTunerHost(existing).ConfigureAwait(false);
-                        }
-                    }
-                }
-            }
-            catch (Exception ex)
-            {
-                _logger.ErrorException("Error saving device", ex);
-            }
-            finally
-            {
-                _semaphore.Release();
-            }
-        }
-
-        private bool UriEquals(string savedUri, string location)
-        {
-            return string.Equals(NormalizeUrl(location), NormalizeUrl(savedUri), StringComparison.OrdinalIgnoreCase);
-        }
-
-        private string NormalizeUrl(string url)
-        {
-            if (!url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
-            {
-                url = "http://" + url;
-            }
-
-            url = url.TrimEnd('/');
-
-            // Strip off the port
-            return new Uri(url).GetComponents(UriComponents.AbsoluteUri & ~UriComponents.Port, UriFormat.UriEscaped);
-        }
-
-        private LiveTvOptions GetConfiguration()
-        {
-            return _config.GetConfiguration<LiveTvOptions>("livetv");
-        }
-
-        public void Dispose()
-        {
-        }
-    }
-}

+ 128 - 128
Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs

@@ -20,6 +20,7 @@ using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Net;
+using MediaBrowser.Model.System;
 
 namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
 {
@@ -30,8 +31,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
         private readonly IServerApplicationHost _appHost;
         private readonly ISocketFactory _socketFactory;
         private readonly INetworkManager _networkManager;
+        private readonly IEnvironmentInfo _environment;
 
-        public HdHomerunHost(IServerConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IHttpClient httpClient, IFileSystem fileSystem, IServerApplicationHost appHost, ISocketFactory socketFactory, INetworkManager networkManager)
+        public HdHomerunHost(IServerConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IHttpClient httpClient, IFileSystem fileSystem, IServerApplicationHost appHost, ISocketFactory socketFactory, INetworkManager networkManager, IEnvironmentInfo environment)
             : base(config, logger, jsonSerializer, mediaEncoder)
         {
             _httpClient = httpClient;
@@ -39,6 +41,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
             _appHost = appHost;
             _socketFactory = socketFactory;
             _networkManager = networkManager;
+            _environment = environment;
         }
 
         public string Name
@@ -56,7 +59,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
             get { return "hdhomerun"; }
         }
 
-        private const string ChannelIdPrefix = "hdhr_";
+        protected override string ChannelIdPrefix
+        {
+            get
+            {
+                return "hdhr_";
+            }
+        }
 
         private string GetChannelId(TunerHostInfo info, Channels i)
         {
@@ -125,7 +134,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
                 DiscoverResponse response;
                 if (_modelCache.TryGetValue(info.Url, out response))
                 {
-                    return response;
+                    if ((DateTime.UtcNow - response.DateQueried).TotalHours <= 12)
+                    {
+                        return response;
+                    }
                 }
             }
 
@@ -135,8 +147,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
                 {
                     Url = string.Format("{0}/discover.json", GetApiUrl(info, false)),
                     CancellationToken = cancellationToken,
-                    CacheLength = TimeSpan.FromDays(1),
-                    CacheMode = CacheMode.Unconditional,
                     TimeoutMs = Convert.ToInt32(TimeSpan.FromSeconds(5).TotalMilliseconds),
                     BufferContent = false
 
@@ -215,7 +225,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
             var list = new List<LiveTvTunerInfo>();
 
             foreach (var host in GetConfiguration().TunerHosts
-                .Where(i => i.IsEnabled && string.Equals(i.Type, Type, StringComparison.OrdinalIgnoreCase)))
+                .Where(i => string.Equals(i.Type, Type, StringComparison.OrdinalIgnoreCase)))
             {
                 try
                 {
@@ -268,6 +278,16 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
             public int HD { get; set; }
         }
 
+        protected EncodingOptions GetEncodingOptions()
+        {
+            return Config.GetConfiguration<EncodingOptions>("encoding");
+        }
+
+        private string GetHdHrIdFromChannelId(string channelId)
+        {
+            return channelId.Split('_')[1];
+        }
+
         private MediaSourceInfo GetMediaSource(TunerHostInfo info, string channelId, ChannelInfo channelInfo, string profile)
         {
             int? width = null;
@@ -355,14 +375,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
                 nal = "0";
             }
 
-            var url = GetApiUrl(info, true) + "/auto/v" + channelId;
-
-            // If raw was used, the tuner doesn't support params
-            if (!string.IsNullOrWhiteSpace(profile)
-                && !string.Equals(profile, "native", StringComparison.OrdinalIgnoreCase))
-            {
-                url += "?transcode=" + profile;
-            }
+            var url = GetApiUrl(info, false);
 
             var id = profile;
             if (string.IsNullOrWhiteSpace(id))
@@ -371,92 +384,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
             }
             id += "_" + url.GetMD5().ToString("N");
 
-            var mediaSource = new MediaSourceInfo
-            {
-                Path = url,
-                Protocol = MediaProtocol.Http,
-                MediaStreams = new List<MediaStream>
-                        {
-                            new MediaStream
-                            {
-                                Type = MediaStreamType.Video,
-                                // Set the index to -1 because we don't know the exact index of the video stream within the container
-                                Index = -1,
-                                IsInterlaced = isInterlaced,
-                                Codec = videoCodec,
-                                Width = width,
-                                Height = height,
-                                BitRate = videoBitrate,
-                                NalLengthSize = nal
-
-                            },
-                            new MediaStream
-                            {
-                                Type = MediaStreamType.Audio,
-                                // Set the index to -1 because we don't know the exact index of the audio stream within the container
-                                Index = -1,
-                                Codec = audioCodec,
-                                BitRate = audioBitrate
-                            }
-                        },
-                RequiresOpening = true,
-                RequiresClosing = false,
-                BufferMs = 0,
-                Container = "ts",
-                Id = id,
-                SupportsDirectPlay = false,
-                SupportsDirectStream = true,
-                SupportsTranscoding = true,
-                IsInfiniteStream = true
-            };
-
-            mediaSource.InferTotalBitrate();
-
-            return mediaSource;
-        }
-
-        protected EncodingOptions GetEncodingOptions()
-        {
-            return Config.GetConfiguration<EncodingOptions>("encoding");
-        }
-
-        private string GetHdHrIdFromChannelId(string channelId)
-        {
-            return channelId.Split('_')[1];
-        }
-
-        private MediaSourceInfo GetLegacyMediaSource(TunerHostInfo info, string channelId, ChannelInfo channel)
-        {
-            int? width = null;
-            int? height = null;
-            bool isInterlaced = true;
-            string videoCodec = null;
-            string audioCodec = null;
-
-            int? videoBitrate = null;
-            int? audioBitrate = null;
-
-            if (channel != null)
-            {
-                if (string.IsNullOrWhiteSpace(videoCodec))
-                {
-                    videoCodec = channel.VideoCodec;
-                }
-                audioCodec = channel.AudioCodec;
-            }
-
-            // normalize
-            if (string.Equals(videoCodec, "mpeg2", StringComparison.OrdinalIgnoreCase))
-            {
-                videoCodec = "mpeg2video";
-            }
-
-            string nal = null;
-
-            var url = GetApiUrl(info, false);
-            var id = channelId;
-            id += "_" + url.GetMD5().ToString("N");
-
             var mediaSource = new MediaSourceInfo
             {
                 Path = url,
@@ -520,7 +447,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
 
             if (isLegacyTuner)
             {
-                list.Add(GetLegacyMediaSource(info, hdhrId, channelInfo));
+                list.Add(GetMediaSource(info, hdhrId, channelInfo, "native"));
             }
             else
             {
@@ -559,26 +486,12 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
             return list;
         }
 
-        protected override bool IsValidChannelId(string channelId)
-        {
-            if (string.IsNullOrWhiteSpace(channelId))
-            {
-                throw new ArgumentNullException("channelId");
-            }
-
-            return channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase);
-        }
-
         protected override async Task<LiveStream> GetChannelStream(TunerHostInfo info, string channelId, string streamId, CancellationToken cancellationToken)
         {
             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))
-            {
-                throw new ArgumentException("Channel not found");
-            }
             var hdhrId = GetHdHrIdFromChannelId(channelId);
 
             var channels = await GetChannels(info, true, CancellationToken.None).ConfigureAwait(false);
@@ -586,30 +499,40 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
 
             var hdhomerunChannel = channelInfo as HdHomerunChannelInfo;
 
-            if (hdhomerunChannel != null && hdhomerunChannel.IsLegacyTuner)
-            {              
-                var mediaSource = GetLegacyMediaSource(info, hdhrId, channelInfo);
-                var modelInfo = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
+            var mediaSource = GetMediaSource(info, hdhrId, channelInfo, profile);
+            var modelInfo = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
 
+            if (hdhomerunChannel != null && hdhomerunChannel.IsLegacyTuner)
+            {
                 return new HdHomerunUdpStream(mediaSource, streamId, new LegacyHdHomerunChannelCommands(hdhomerunChannel.Url), modelInfo.TunerCount, _fileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost, _socketFactory, _networkManager);
             }
-            else
+
+            // The UDP method is not working reliably on OSX, and on BSD it hasn't been tested yet
+            var enableHttpStream = _environment.OperatingSystem == OperatingSystem.OSX ||
+                _environment.OperatingSystem == OperatingSystem.BSD;
+            enableHttpStream = true;
+            if (enableHttpStream)
             {
-                var mediaSource = GetMediaSource(info, hdhrId, channelInfo, profile);
-                //var modelInfo = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
+                mediaSource.Protocol = MediaProtocol.Http;
+
+                var httpUrl = GetApiUrl(info, true) + "/auto/v" + hdhrId;
+
+                // If raw was used, the tuner doesn't support params
+                if (!string.IsNullOrWhiteSpace(profile)
+                    && !string.Equals(profile, "native", StringComparison.OrdinalIgnoreCase))
+                {
+                    httpUrl += "?transcode=" + profile;
+                }
+                mediaSource.Path = httpUrl;
 
                 return new HdHomerunHttpStream(mediaSource, streamId, _fileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost);
-                //return new HdHomerunUdpStream(mediaSource, streamId, new HdHomerunChannelCommands(hdhomerunChannel.Number), modelInfo.TunerCount, _fileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost, _socketFactory, _networkManager);
             }
+
+            return new HdHomerunUdpStream(mediaSource, streamId, new HdHomerunChannelCommands(hdhomerunChannel.Number), modelInfo.TunerCount, _fileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost, _socketFactory, _networkManager);
         }
 
         public async Task Validate(TunerHostInfo info)
         {
-            if (!info.IsEnabled)
-            {
-                return;
-            }
-
             lock (_modelCache)
             {
                 _modelCache.Clear();
@@ -651,6 +574,83 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
             public string BaseURL { get; set; }
             public string LineupURL { get; set; }
             public int TunerCount { get; set; }
+
+            public DateTime DateQueried { get; set; }
+
+            public DiscoverResponse()
+            {
+                DateQueried = DateTime.UtcNow;
+            }
+        }
+
+        public async Task<List<TunerHostInfo>> DiscoverDevices(int discoveryDurationMs, CancellationToken cancellationToken)
+        {
+            cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(new CancellationTokenSource(discoveryDurationMs).Token, cancellationToken).Token;
+            var list = new List<TunerHostInfo>();
+
+            // Create udp broadcast discovery message
+            byte[] discBytes = { 0, 2, 0, 12, 1, 4, 255, 255, 255, 255, 2, 4, 255, 255, 255, 255, 115, 204, 125, 143 };
+            using (var udpClient = _socketFactory.CreateUdpBroadcastSocket(0))
+            {
+                // Need a way to set the Receive timeout on the socket otherwise this might never timeout?
+                try
+                {
+                    await udpClient.SendAsync(discBytes, discBytes.Length, new IpEndPointInfo(new IpAddressInfo("255.255.255.255", IpAddressFamily.InterNetwork), 65001), cancellationToken);
+                    while (!cancellationToken.IsCancellationRequested)
+                    {
+                        var response = await udpClient.ReceiveAsync(cancellationToken).ConfigureAwait(false);
+                        var deviceIp = response.RemoteEndPoint.IpAddress.Address;
+
+                        // check to make sure we have enough bytes received to be a valid message and make sure the 2nd byte is the discover reply byte
+                        if (response.ReceivedBytes > 13 && response.Buffer[1] == 3)
+                        {
+                            var deviceAddress = "http://" + deviceIp;
+
+                            var info = await TryGetTunerHostInfo(deviceAddress, cancellationToken).ConfigureAwait(false);
+
+                            if (info != null)
+                            {
+                                list.Add(info);
+                            }
+                        }
+                    }
+
+                }
+                catch (OperationCanceledException)
+                {
+                }
+                catch
+                {
+                    // Socket timeout indicates all messages have been received.
+                }
+            }
+
+            return list;
+        }
+
+        private async Task<TunerHostInfo> TryGetTunerHostInfo(string url, CancellationToken cancellationToken)
+        {
+            var hostInfo = new TunerHostInfo
+            {
+                Type = Type,
+                Url = url
+            };
+
+            try
+            {
+                var modelInfo = await GetModelInfo(hostInfo, false, cancellationToken).ConfigureAwait(false);
+
+                hostInfo.DeviceId = modelInfo.DeviceID;
+                hostInfo.FriendlyName = modelInfo.FriendlyName;
+
+                return hostInfo;
+            }
+            catch
+            {
+                // logged at lower levels
+            }
+
+            return null;
         }
     }
 }

+ 6 - 7
Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs

@@ -120,7 +120,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
                                 // send url to start streaming
                                 await hdHomerunManager.StartStreaming(remoteAddress, localAddress, localPort, _channelCommands, _numTuners, cancellationToken).ConfigureAwait(false);
 
-                                var response = await udpClient.ReceiveAsync(cancellationToken).ConfigureAwait(false);
                                 _logger.Info("Opened HDHR UDP stream from {0}", remoteAddress);
 
                                 if (!cancellationToken.IsCancellationRequested)
@@ -131,12 +130,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
                                         onStarted = () => openTaskCompletionSource.TrySetResult(true);
                                     }
 
-                                    var stream = new UdpClientStream(udpClient);
-                                    await _multicastStream.CopyUntilCancelled(stream, onStarted, cancellationToken).ConfigureAwait(false);
+                                    await _multicastStream.CopyUntilCancelled(new UdpClientStream(udpClient), onStarted, cancellationToken).ConfigureAwait(false);
                                 }
                             }
-                            catch (OperationCanceledException)
+                            catch (OperationCanceledException ex)
                             {
+                                _logger.Info("HDHR UDP stream cancelled or timed out from {0}", remoteAddress);
+                                openTaskCompletionSource.TrySetException(ex);
                                 break;
                             }
                             catch (Exception ex)
@@ -155,7 +155,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
                         }
 
                         await hdHomerunManager.StopStreaming().ConfigureAwait(false);
-                        udpClient.Dispose();
                         _liveStreamTaskCompletionSource.TrySetResult(true);
                     }
                 }
@@ -207,7 +206,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
                 var data = await _udpClient.ReceiveAsync(cancellationToken).ConfigureAwait(false);
 
                 var bytesRead = data.ReceivedBytes - RtpHeaderBytes;
-
+                
                 // remove rtp header
                 Buffer.BlockCopy(data.Buffer, RtpHeaderBytes, buffer, offset, bytesRead);
                 offset += bytesRead;
@@ -291,4 +290,4 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
             throw new NotImplementedException();
         }
     }
-}
+}

+ 5 - 12
Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs

@@ -46,8 +46,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
             get { return "M3U Tuner"; }
         }
 
-        private const string ChannelIdPrefix = "m3u_";
-
         protected override async Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo info, CancellationToken cancellationToken)
         {
             var result = await new M3uParser(Logger, _fileSystem, _httpClient, _appHost).Parse(info.Url, ChannelIdPrefix, info.Id, !info.EnableTvgId, cancellationToken).ConfigureAwait(false);
@@ -87,16 +85,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
             }
         }
 
-        protected override bool IsValidChannelId(string channelId)
-        {
-            if (string.IsNullOrWhiteSpace(channelId))
-            {
-                throw new ArgumentNullException("channelId");
-            }
-
-            return channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase);
-        }
-
         protected override async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo info, string channelId, CancellationToken cancellationToken)
         {
             var urlHash = info.Url.GetMD5().ToString("N");
@@ -176,5 +164,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
         {
             return Task.FromResult(true);
         }
+
+        public Task<List<TunerHostInfo>> DiscoverDevices(int discoveryDurationMs, CancellationToken cancellationToken)
+        {
+            return Task.FromResult(new List<TunerHostInfo>());
+        }
     }
 }

+ 65 - 4
Emby.Server.Implementations/LiveTv/TunerHosts/MulticastStream.cs

@@ -7,6 +7,7 @@ using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Net;
 
 namespace Emby.Server.Implementations.LiveTv.TunerHosts
 {
@@ -34,13 +35,73 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
 
                 if (bytesRead > 0)
                 {
-                    byte[] copy = new byte[bytesRead];
-                    Buffer.BlockCopy(buffer, 0, copy, 0, bytesRead);
+                    var allStreams = _outputStreams.ToList();
+
+                    if (allStreams.Count == 1)
+                    {
+                        await allStreams[0].Value.WriteAsync(buffer, 0, bytesRead).ConfigureAwait(false);
+                    }
+                    else
+                    {
+                        byte[] copy = new byte[bytesRead];
+                        Buffer.BlockCopy(buffer, 0, copy, 0, bytesRead);
+
+                        foreach (var stream in allStreams)
+                        {
+                            stream.Value.Queue(copy, 0, copy.Length);
+                        }
+                    }
+
+                    if (onStarted != null)
+                    {
+                        var onStartedCopy = onStarted;
+                        onStarted = null;
+                        Task.Run(onStartedCopy);
+                    }
+                }
+
+                else
+                {
+                    await Task.Delay(100).ConfigureAwait(false);
+                }
+            }
+        }
+
+        private static int RtpHeaderBytes = 12;
+        public async Task CopyUntilCancelled(ISocket udpClient, Action onStarted, CancellationToken cancellationToken)
+        {
+            _cancellationToken = cancellationToken;
 
+            while (!cancellationToken.IsCancellationRequested)
+            {
+                var receiveToken = cancellationToken;
+
+                // On the first connection attempt, put a timeout to avoid being stuck indefinitely in the event of failure
+                if (onStarted != null)
+                {
+                    receiveToken = CancellationTokenSource.CreateLinkedTokenSource(new CancellationTokenSource(5000).Token, cancellationToken).Token;
+                }
+
+                var data = await udpClient.ReceiveAsync(receiveToken).ConfigureAwait(false);
+                var bytesRead = data.ReceivedBytes - RtpHeaderBytes;
+
+                if (bytesRead > 0)
+                {
                     var allStreams = _outputStreams.ToList();
-                    foreach (var stream in allStreams)
+
+                    if (allStreams.Count == 1)
+                    {
+                        await allStreams[0].Value.WriteAsync(data.Buffer, 0, bytesRead).ConfigureAwait(false);
+                    }
+                    else
                     {
-                        stream.Value.Queue(copy);
+                        byte[] copy = new byte[bytesRead];
+                        Buffer.BlockCopy(data.Buffer, RtpHeaderBytes, copy, 0, bytesRead);
+
+                        foreach (var stream in allStreams)
+                        {
+                            stream.Value.Queue(copy, 0, copy.Length);
+                        }
                     }
 
                     if (onStarted != null)

+ 43 - 14
Emby.Server.Implementations/LiveTv/TunerHosts/QueueStream.cs

@@ -13,7 +13,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
     public class QueueStream
     {
         private readonly Stream _outputStream;
-        private readonly ConcurrentQueue<byte[]> _queue = new ConcurrentQueue<byte[]>();
+        private readonly ConcurrentQueue<Tuple<byte[], int, int>> _queue = new ConcurrentQueue<Tuple<byte[], int, int>>();
         private CancellationToken _cancellationToken;
         public TaskCompletionSource<bool> TaskCompletion { get; private set; }
 
@@ -28,9 +28,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
             TaskCompletion = new TaskCompletionSource<bool>();
         }
 
-        public void Queue(byte[] bytes)
+        public void Queue(byte[] bytes, int offset, int count)
         {
-            _queue.Enqueue(bytes);
+            _queue.Enqueue(new Tuple<byte[], int, int>(bytes, offset, count));
         }
 
         public void Start(CancellationToken cancellationToken)
@@ -39,17 +39,49 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
             Task.Run(() => StartInternal());
         }
 
-        private byte[] Dequeue()
+        private Tuple<byte[], int, int> Dequeue()
         {
-            byte[] bytes;
-            if (_queue.TryDequeue(out bytes))
+            Tuple<byte[], int, int> result;
+            if (_queue.TryDequeue(out result))
             {
-                return bytes;
+                return result;
             }
 
             return null;
         }
 
+        private void OnClosed()
+        {
+            GC.Collect();
+            if (OnFinished != null)
+            {
+                OnFinished(this);
+            }
+        }
+
+        public async Task WriteAsync(byte[] bytes, int offset, int count)
+        {
+            //return _outputStream.WriteAsync(bytes, offset, count, cancellationToken);
+            var cancellationToken = _cancellationToken;
+
+            try
+            {
+                await _outputStream.WriteAsync(bytes, offset, count, cancellationToken).ConfigureAwait(false);
+            }
+            catch (OperationCanceledException)
+            {
+                _logger.Debug("QueueStream cancelled");
+                TaskCompletion.TrySetCanceled();
+                OnClosed();
+            }
+            catch (Exception ex)
+            {
+                _logger.ErrorException("Error in QueueStream", ex);
+                TaskCompletion.TrySetException(ex);
+                OnClosed();
+            }
+        }
+
         private async Task StartInternal()
         {
             var cancellationToken = _cancellationToken;
@@ -58,10 +90,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
             {
                 while (true)
                 {
-                    var bytes = Dequeue();
-                    if (bytes != null)
+                    var result = Dequeue();
+                    if (result != null)
                     {
-                        await _outputStream.WriteAsync(bytes, 0, bytes.Length, cancellationToken).ConfigureAwait(false);
+                        await _outputStream.WriteAsync(result.Item1, result.Item2, result.Item3, cancellationToken).ConfigureAwait(false);
                     }
                     else
                     {
@@ -81,10 +113,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
             }
             finally
             {
-                if (OnFinished != null)
-                {
-                    OnFinished(this);
-                }
+                OnClosed();
             }
         }
     }

+ 1 - 0
Emby.Server.Implementations/Localization/LocalizationManager.cs

@@ -408,6 +408,7 @@ namespace Emby.Server.Implementations.Localization
                 new LocalizatonOption{ Name="Italian", Value="it"},
                 new LocalizatonOption{ Name="Kazakh", Value="kk"},
                 new LocalizatonOption{ Name="Norwegian Bokmål", Value="nb"},
+                new LocalizatonOption{ Name="Persian", Value="fa"},
                 new LocalizatonOption{ Name="Polish", Value="pl"},
                 new LocalizatonOption{ Name="Portuguese (Brazil)", Value="pt-BR"},
                 new LocalizatonOption{ Name="Portuguese (Portugal)", Value="pt-PT"},

+ 32 - 0
Emby.Server.Implementations/Playlists/PlaylistImageProvider.cs

@@ -11,6 +11,7 @@ using System.Linq;
 using System.Threading.Tasks;
 using Emby.Server.Implementations.Images;
 using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.IO;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Controller.Library;
@@ -101,4 +102,35 @@ namespace Emby.Server.Implementations.Playlists
         //}
     }
 
+    public class GenreImageProvider : BaseDynamicImageProvider<Genre>
+    {
+        private readonly ILibraryManager _libraryManager;
+
+        public GenreImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager) : base(fileSystem, providerManager, applicationPaths, imageProcessor)
+        {
+            _libraryManager = libraryManager;
+        }
+
+        protected override Task<List<BaseItem>> GetItemsWithImages(IHasImages item)
+        {
+            var items = _libraryManager.GetItemList(new InternalItemsQuery
+            {
+                Genres = new[] { item.Name },
+                IncludeItemTypes = new[] { typeof(Series).Name, typeof(Movie).Name },
+                SortBy = new[] { ItemSortBy.Random },
+                Limit = 4,
+                Recursive = true,
+                ImageTypes = new[] { ImageType.Primary }
+
+            }).ToList();
+
+            return Task.FromResult(GetFinalItems(items));
+        }
+
+        //protected override Task<string> CreateImage(IHasImages item, List<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex)
+        //{
+        //    return CreateSingleImage(itemsWithImages, outputPathWithoutExtension, ImageType.Primary);
+        //}
+    }
+
 }

+ 2 - 0
Emby.Server.Implementations/Security/PluginSecurityManager.cs

@@ -301,10 +301,12 @@ namespace Emby.Server.Implementations.Security
 
                     if (reg.registered)
                     {
+                        _logger.Info("Registered for feature {0}", feature);
                         LicenseFile.AddRegCheck(feature, reg.expDate);
                     }
                     else
                     {
+                        _logger.Info("Not registered for feature {0}", feature);
                         LicenseFile.RemoveRegCheck(feature);
                     }
 

+ 39 - 43
Emby.Server.Implementations/Services/ResponseHelper.cs

@@ -12,45 +12,6 @@ namespace Emby.Server.Implementations.Services
 {
     public static class ResponseHelper
     {
-        private static async Task<bool> WriteToOutputStream(IResponse response, object result)
-        {
-            var asyncStreamWriter = result as IAsyncStreamWriter;
-            if (asyncStreamWriter != null)
-            {
-                await asyncStreamWriter.WriteToAsync(response.OutputStream, CancellationToken.None).ConfigureAwait(false);
-                return true;
-            }
-
-            var streamWriter = result as IStreamWriter;
-            if (streamWriter != null)
-            {
-                streamWriter.WriteTo(response.OutputStream);
-                return true;
-            }
-
-            var stream = result as Stream;
-            if (stream != null)
-            {
-                using (stream)
-                {
-                    await stream.CopyToAsync(response.OutputStream).ConfigureAwait(false);
-                    return true;
-                }
-            }
-
-            var bytes = result as byte[];
-            if (bytes != null)
-            {
-                response.ContentType = "application/octet-stream";
-                response.SetContentLength(bytes.Length);
-
-                await response.OutputStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
-                return true;
-            }
-
-            return false;
-        }
-
         public static Task WriteToResponse(IResponse httpRes, IRequest httpReq, object result)
         {
             if (result == null)
@@ -141,16 +102,51 @@ namespace Emby.Server.Implementations.Services
                 response.ContentType += "; charset=utf-8";
             }
 
-            var writeToOutputStreamResult = await WriteToOutputStream(response, result).ConfigureAwait(false);
-            if (writeToOutputStreamResult)
+            var asyncStreamWriter = result as IAsyncStreamWriter;
+            if (asyncStreamWriter != null)
             {
+                await asyncStreamWriter.WriteToAsync(response.OutputStream, CancellationToken.None).ConfigureAwait(false);
+                return;
+            }
+
+            var streamWriter = result as IStreamWriter;
+            if (streamWriter != null)
+            {
+                streamWriter.WriteTo(response.OutputStream);
+                return;
+            }
+
+            var fileWriter = result as FileWriter;
+            if (fileWriter != null)
+            {
+                await fileWriter.WriteToAsync(response, CancellationToken.None).ConfigureAwait(false);
+                return;
+            }
+
+            var stream = result as Stream;
+            if (stream != null)
+            {
+                using (stream)
+                {
+                    await stream.CopyToAsync(response.OutputStream).ConfigureAwait(false);
+                    return;
+                }
+            }
+
+            var bytes = result as byte[];
+            if (bytes != null)
+            {
+                response.ContentType = "application/octet-stream";
+                response.SetContentLength(bytes.Length);
+
+                await response.OutputStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
                 return;
             }
 
             var responseText = result as string;
             if (responseText != null)
             {
-                var bytes = Encoding.UTF8.GetBytes(responseText);
+                bytes = Encoding.UTF8.GetBytes(responseText);
                 response.SetContentLength(bytes.Length);
                 await response.OutputStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
                 return;
@@ -163,7 +159,7 @@ namespace Emby.Server.Implementations.Services
         {
             var contentType = request.ResponseContentType;
             var serializer = RequestHelper.GetResponseWriter(HttpListenerHost.Instance, contentType);
-            
+
             using (var ms = new MemoryStream())
             {
                 serializer(result, ms);

+ 17 - 12
Emby.Server.Implementations/TV/TVSeriesManager.cs

@@ -77,7 +77,7 @@ namespace Emby.Server.Implementations.TV
             // Avoid implicitly captured closure
             var episodes = GetNextUpEpisodes(request, user, items);
 
-            return GetResult(episodes, null, request);
+            return GetResult(episodes, request);
         }
 
         public QueryResult<BaseItem> GetNextUp(NextUpQuery request, List<Folder> parentsFolders)
@@ -128,7 +128,7 @@ namespace Emby.Server.Implementations.TV
             // Avoid implicitly captured closure
             var episodes = GetNextUpEpisodes(request, user, items);
 
-            return GetResult(episodes, null, request);
+            return GetResult(episodes, request);
         }
 
         public IEnumerable<Episode> GetNextUpEpisodes(NextUpQuery request, User user, IEnumerable<string> seriesKeys)
@@ -163,8 +163,7 @@ namespace Emby.Server.Implementations.TV
                     return false;
                 })
                 .Select(i => i.Item2())
-                .Where(i => i != null)
-                .Take(request.Limit ?? int.MaxValue);
+                .Where(i => i != null);
         }
 
         private string GetUniqueSeriesKey(BaseItem series)
@@ -232,24 +231,30 @@ namespace Emby.Server.Implementations.TV
             return new Tuple<DateTime, Func<Episode>>(DateTime.MinValue, getEpisode);
         }
 
-        private QueryResult<BaseItem> GetResult(IEnumerable<BaseItem> items, int? totalRecordLimit, NextUpQuery query)
+        private QueryResult<BaseItem> GetResult(IEnumerable<BaseItem> items, NextUpQuery query)
         {
-            var itemsArray = totalRecordLimit.HasValue ? items.Take(totalRecordLimit.Value).ToArray() : items.ToArray();
-            var totalCount = itemsArray.Length;
+            int totalCount = 0;
 
-            if (query.Limit.HasValue)
+            if (query.EnableTotalRecordCount)
             {
-                itemsArray = itemsArray.Skip(query.StartIndex ?? 0).Take(query.Limit.Value).ToArray();
+                var list = items.ToList();
+                totalCount = list.Count;
+                items = list;
             }
-            else if (query.StartIndex.HasValue)
+
+            if (query.StartIndex.HasValue)
+            {
+                items = items.Skip(query.StartIndex.Value);
+            }
+            if (query.Limit.HasValue)
             {
-                itemsArray = itemsArray.Skip(query.StartIndex.Value).ToArray();
+                items = items.Take(query.Limit.Value);
             }
 
             return new QueryResult<BaseItem>
             {
                 TotalRecordCount = totalCount,
-                Items = itemsArray
+                Items = items.ToArray()
             };
         }
     }

+ 11 - 15
Emby.Server.Implementations/Udp/UdpServer.cs

@@ -203,19 +203,6 @@ namespace Emby.Server.Implementations.Udp
             GC.SuppressFinalize(this);
         }
 
-        /// <summary>
-        /// Stops this instance.
-        /// </summary>
-        public void Stop()
-        {
-            _isDisposed = true;
-
-            if (_udpClient != null)
-            {
-                _udpClient.Dispose();
-            }
-        }
-
         /// <summary>
         /// Releases unmanaged and - optionally - managed resources.
         /// </summary>
@@ -224,7 +211,12 @@ namespace Emby.Server.Implementations.Udp
         {
             if (dispose)
             {
-                Stop();
+                _isDisposed = true;
+
+                if (_udpClient != null)
+                {
+                    _udpClient.Dispose();
+                }
             }
         }
 
@@ -247,9 +239,13 @@ namespace Emby.Server.Implementations.Udp
 
             try
             {
-                await _udpClient.SendAsync(bytes, bytes.Length, remoteEndPoint, CancellationToken.None).ConfigureAwait(false);
+                await _udpClient.SendWithLockAsync(bytes, bytes.Length, remoteEndPoint, CancellationToken.None).ConfigureAwait(false);
 
                 _logger.Info("Udp message sent to {0}", remoteEndPoint);
+            }
+            catch (OperationCanceledException)
+            {
+                
             }
             catch (Exception ex)
             {

+ 1 - 1
Emby.Server.Implementations/packages.config

@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <packages>
   <package id="Emby.XmlTv" version="1.0.7" targetFramework="portable45-net45+win8" />
-  <package id="MediaBrowser.Naming" version="1.0.4" targetFramework="portable45-net45+win8" />
+  <package id="MediaBrowser.Naming" version="1.0.5" targetFramework="portable45-net45+win8" />
   <package id="SQLitePCL.pretty" version="1.1.0" targetFramework="portable45-net45+win8" />
   <package id="SQLitePCLRaw.core" version="1.1.2" targetFramework="portable45-net45+win8" />
   <package id="UniversalDetector" version="1.0.1" targetFramework="portable45-net45+win8" />

+ 33 - 33
MediaBrowser.Api/LiveTv/LiveTvService.cs

@@ -582,13 +582,13 @@ namespace MediaBrowser.Api.LiveTv
     }
 
     [Route("/LiveTv/ListingProviders/Default", "GET")]
-    [Authenticated(AllowBeforeStartupWizard = true)]
+    [Authenticated]
     public class GetDefaultListingProvider : ListingsProviderInfo, IReturn<ListingsProviderInfo>
     {
     }
 
     [Route("/LiveTv/ListingProviders", "POST", Summary = "Adds a listing provider")]
-    [Authenticated(AllowBeforeStartupWizard = true)]
+    [Authenticated]
     public class AddListingProvider : ListingsProviderInfo, IReturn<ListingsProviderInfo>
     {
         public bool ValidateLogin { get; set; }
@@ -596,7 +596,7 @@ namespace MediaBrowser.Api.LiveTv
     }
 
     [Route("/LiveTv/ListingProviders", "DELETE", Summary = "Deletes a listing provider")]
-    [Authenticated(AllowBeforeStartupWizard = true)]
+    [Authenticated]
     public class DeleteListingProvider : IReturnVoid
     {
         [ApiMember(Name = "Id", Description = "Provider id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "DELETE")]
@@ -604,7 +604,7 @@ namespace MediaBrowser.Api.LiveTv
     }
 
     [Route("/LiveTv/ListingProviders/Lineups", "GET", Summary = "Gets available lineups")]
-    [Authenticated(AllowBeforeStartupWizard = true)]
+    [Authenticated]
     public class GetLineups : IReturn<List<NameIdPair>>
     {
         [ApiMember(Name = "Id", Description = "Provider id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
@@ -621,13 +621,13 @@ namespace MediaBrowser.Api.LiveTv
     }
 
     [Route("/LiveTv/ListingProviders/SchedulesDirect/Countries", "GET", Summary = "Gets available lineups")]
-    [Authenticated(AllowBeforeStartupWizard = true)]
+    [Authenticated]
     public class GetSchedulesDirectCountries
     {
     }
 
     [Route("/LiveTv/ChannelMappingOptions")]
-    [Authenticated(AllowBeforeStartupWizard = true)]
+    [Authenticated]
     public class GetChannelMappingOptions
     {
         [ApiMember(Name = "Id", Description = "Provider id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
@@ -635,7 +635,7 @@ namespace MediaBrowser.Api.LiveTv
     }
 
     [Route("/LiveTv/ChannelMappings")]
-    [Authenticated(AllowBeforeStartupWizard = true)]
+    [Authenticated]
     public class SetChannelMapping
     {
         [ApiMember(Name = "Id", Description = "Provider id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
@@ -660,20 +660,6 @@ namespace MediaBrowser.Api.LiveTv
         public string Feature { get; set; }
     }
 
-    [Route("/LiveTv/TunerHosts/Satip/IniMappings", "GET", Summary = "Gets available mappings")]
-    [Authenticated(AllowBeforeStartupWizard = true)]
-    public class GetSatIniMappings : IReturn<List<NameValuePair>>
-    {
-
-    }
-
-    [Route("/LiveTv/TunerHosts/Satip/ChannelScan", "GET", Summary = "Scans for available channels")]
-    [Authenticated(AllowBeforeStartupWizard = true)]
-    public class GetSatChannnelScanResult : TunerHostInfo
-    {
-
-    }
-
     [Route("/LiveTv/LiveStreamFiles/{Id}/stream.{Container}", "GET", Summary = "Gets a live tv channel")]
     public class GetLiveStreamFile
     {
@@ -687,6 +673,20 @@ namespace MediaBrowser.Api.LiveTv
         public string Id { get; set; }
     }
 
+    [Route("/LiveTv/TunerHosts/Types", "GET")]
+    [Authenticated]
+    public class GetTunerHostTypes : IReturn<List<NameIdPair>>
+    {
+
+    }
+
+    [Route("/LiveTv/Tuners/Discvover", "GET")]
+    [Authenticated]
+    public class DiscoverTuners : IReturn<List<TunerHostInfo>>
+    {
+        public bool NewDevicesOnly { get; set; }
+    }
+
     public class LiveTvService : BaseApiService
     {
         private readonly ILiveTvManager _liveTvManager;
@@ -712,6 +712,12 @@ namespace MediaBrowser.Api.LiveTv
             _sessionContext = sessionContext;
         }
 
+        public object Get(GetTunerHostTypes request)
+        {
+            var list = _liveTvManager.GetTunerHostTypes();
+            return ToOptimizedResult(list);
+        }
+
         public object Get(GetLiveRecordingFile request)
         {
             var path = _liveTvManager.GetEmbyTvActiveRecordingPath(request.Id);
@@ -731,6 +737,12 @@ namespace MediaBrowser.Api.LiveTv
             };
         }
 
+        public async Task<object> Get(DiscoverTuners request)
+        {
+            var result = await _liveTvManager.DiscoverTuners(request.NewDevicesOnly, CancellationToken.None).ConfigureAwait(false);
+            return ToOptimizedResult(result);
+        }
+
         public async Task<object> Get(GetLiveStreamFile request)
         {
             var directStreamProvider = (await _liveTvManager.GetEmbyTvLiveStream(request.Id).ConfigureAwait(false)) as IDirectStreamProvider;
@@ -749,13 +761,6 @@ namespace MediaBrowser.Api.LiveTv
             return ToOptimizedResult(new ListingsProviderInfo());
         }
 
-        public async Task<object> Get(GetSatChannnelScanResult request)
-        {
-            var result = await _liveTvManager.GetSatChannelScanResult(request, CancellationToken.None).ConfigureAwait(false);
-
-            return ToOptimizedResult(result);
-        }
-
         public async Task<object> Get(GetLiveTvRegistrationInfo request)
         {
             var result = await _liveTvManager.GetRegistrationInfo(request.Feature).ConfigureAwait(false);
@@ -803,11 +808,6 @@ namespace MediaBrowser.Api.LiveTv
             return ToOptimizedResult(result);
         }
 
-        public object Get(GetSatIniMappings request)
-        {
-            return ToOptimizedResult(_liveTvManager.GetSatIniMappings());
-        }
-
         public async Task<object> Get(GetSchedulesDirectCountries request)
         {
             // https://json.schedulesdirect.org/20141201/available/countries

+ 17 - 129
MediaBrowser.Api/Playback/BaseStreamingService.cs

@@ -289,8 +289,10 @@ namespace MediaBrowser.Api.Playback
             // MUST read both stdout and stderr asynchronously or a deadlock may occurr
             //process.BeginOutputReadLine();
 
+            state.TranscodingJob = transcodingJob;
+
             // Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback
-            var task = Task.Run(() => StartStreamingLog(transcodingJob, state, process.StandardError.BaseStream, state.LogFileStream));
+            new JobLogger(Logger).StartStreamingLog(state, process.StandardError.BaseStream, state.LogFileStream);
 
             // Wait for the file to exist before proceeeding
             while (!FileSystem.FileExists(state.WaitForPath ?? outputPath) && !transcodingJob.HasExited)
@@ -340,134 +342,6 @@ namespace MediaBrowser.Api.Playback
             //    string.Equals(GetVideoEncoder(state), "libx264", StringComparison.OrdinalIgnoreCase);
         }
 
-        private async Task StartStreamingLog(TranscodingJob transcodingJob, StreamState state, Stream source, Stream target)
-        {
-            try
-            {
-                using (var reader = new StreamReader(source))
-                {
-                    while (!reader.EndOfStream)
-                    {
-                        var line = await reader.ReadLineAsync().ConfigureAwait(false);
-
-                        ParseLogLine(line, transcodingJob, state);
-
-                        var bytes = Encoding.UTF8.GetBytes(Environment.NewLine + line);
-
-                        await target.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
-                        await target.FlushAsync().ConfigureAwait(false);
-                    }
-                }
-            }
-            catch (ObjectDisposedException)
-            {
-                // Don't spam the log. This doesn't seem to throw in windows, but sometimes under linux
-            }
-            catch (Exception ex)
-            {
-                Logger.ErrorException("Error reading ffmpeg log", ex);
-            }
-        }
-
-        private void ParseLogLine(string line, TranscodingJob transcodingJob, StreamState state)
-        {
-            float? framerate = null;
-            double? percent = null;
-            TimeSpan? transcodingPosition = null;
-            long? bytesTranscoded = null;
-            int? bitRate = null;
-
-            var parts = line.Split(' ');
-
-            var totalMs = state.RunTimeTicks.HasValue
-                ? TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalMilliseconds
-                : 0;
-
-            var startMs = state.Request.StartTimeTicks.HasValue
-                ? TimeSpan.FromTicks(state.Request.StartTimeTicks.Value).TotalMilliseconds
-                : 0;
-
-            for (var i = 0; i < parts.Length; i++)
-            {
-                var part = parts[i];
-
-                if (string.Equals(part, "fps=", StringComparison.OrdinalIgnoreCase) &&
-                    (i + 1 < parts.Length))
-                {
-                    var rate = parts[i + 1];
-                    float val;
-
-                    if (float.TryParse(rate, NumberStyles.Any, UsCulture, out val))
-                    {
-                        framerate = val;
-                    }
-                }
-                else if (state.RunTimeTicks.HasValue &&
-                    part.StartsWith("time=", StringComparison.OrdinalIgnoreCase))
-                {
-                    var time = part.Split(new[] { '=' }, 2).Last();
-                    TimeSpan val;
-
-                    if (TimeSpan.TryParse(time, UsCulture, out val))
-                    {
-                        var currentMs = startMs + val.TotalMilliseconds;
-
-                        var percentVal = currentMs / totalMs;
-                        percent = 100 * percentVal;
-
-                        transcodingPosition = val;
-                    }
-                }
-                else if (part.StartsWith("size=", StringComparison.OrdinalIgnoreCase))
-                {
-                    var size = part.Split(new[] { '=' }, 2).Last();
-
-                    int? scale = null;
-                    if (size.IndexOf("kb", StringComparison.OrdinalIgnoreCase) != -1)
-                    {
-                        scale = 1024;
-                        size = size.Replace("kb", string.Empty, StringComparison.OrdinalIgnoreCase);
-                    }
-
-                    if (scale.HasValue)
-                    {
-                        long val;
-
-                        if (long.TryParse(size, NumberStyles.Any, UsCulture, out val))
-                        {
-                            bytesTranscoded = val * scale.Value;
-                        }
-                    }
-                }
-                else if (part.StartsWith("bitrate=", StringComparison.OrdinalIgnoreCase))
-                {
-                    var rate = part.Split(new[] { '=' }, 2).Last();
-
-                    int? scale = null;
-                    if (rate.IndexOf("kbits/s", StringComparison.OrdinalIgnoreCase) != -1)
-                    {
-                        scale = 1024;
-                        rate = rate.Replace("kbits/s", string.Empty, StringComparison.OrdinalIgnoreCase);
-                    }
-
-                    if (scale.HasValue)
-                    {
-                        float val;
-
-                        if (float.TryParse(rate, NumberStyles.Any, UsCulture, out val))
-                        {
-                            bitRate = (int)Math.Ceiling(val * scale.Value);
-                        }
-                    }
-                }
-            }
-
-            if (framerate.HasValue || percent.HasValue)
-            {
-                ApiEntryPoint.Instance.ReportTranscodingProgress(transcodingJob, state, transcodingPosition, framerate, percent, bytesTranscoded, bitRate);
-            }
-        }
-
         /// <summary>
         /// Processes the exited.
         /// </summary>
@@ -697,6 +571,20 @@ namespace MediaBrowser.Api.Playback
                 {
                     request.SubtitleCodec = val;
                 }
+                else if (i == 31)
+                {
+                    if (videoRequest != null)
+                    {
+                        videoRequest.RequireNonAnamorphic = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
+                    }
+                }
+                else if (i == 32)
+                {
+                    if (videoRequest != null)
+                    {
+                        videoRequest.DeInterlace = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
+                    }
+                }
             }
         }
 

+ 30 - 10
MediaBrowser.Api/Playback/Hls/BaseHlsService.cs

@@ -41,9 +41,16 @@ namespace MediaBrowser.Api.Playback.Hls
         /// <summary>
         /// Gets the segment file extension.
         /// </summary>
-        /// <param name="state">The state.</param>
-        /// <returns>System.String.</returns>
-        protected abstract string GetSegmentFileExtension(StreamState state);
+        protected string GetSegmentFileExtension(StreamRequest request)
+        {
+            var segmentContainer = request.SegmentContainer;
+            if (!string.IsNullOrWhiteSpace(segmentContainer))
+            {
+                return "." + segmentContainer;
+            }
+
+            return ".ts";
+        }
 
         /// <summary>
         /// Gets the type of the transcoding job.
@@ -103,8 +110,11 @@ namespace MediaBrowser.Api.Playback.Hls
                             throw;
                         }
 
-                        var waitForSegments = state.SegmentLength >= 10 ? 2 : 3;
-                        await WaitForMinimumSegmentCount(playlist, waitForSegments, cancellationTokenSource.Token).ConfigureAwait(false);
+                        var minSegments = state.MinSegments;
+                        if (minSegments > 0)
+                        {
+                            await WaitForMinimumSegmentCount(playlist, minSegments, cancellationTokenSource.Token).ConfigureAwait(false);
+                        }
                     }
                 }
                 finally
@@ -258,14 +268,22 @@ namespace MediaBrowser.Api.Playback.Hls
                     "hls/" + Path.GetFileNameWithoutExtension(outputPath));
             }
 
-            var useGenericSegmenter = false;
+            var useGenericSegmenter = true;
             if (useGenericSegmenter)
             {
-                var outputTsArg = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d" + GetSegmentFileExtension(state);
+                var outputTsArg = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d" + GetSegmentFileExtension(state.Request);
 
                 var timeDeltaParam = String.Empty;
 
-                return string.Format("{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -f segment -max_delay 5000000 -avoid_negative_ts disabled -start_at_zero -segment_time {6} {10} -individual_header_trailer 0 -segment_format mpegts -segment_list_type m3u8 -segment_start_number {7} -segment_list \"{8}\" -y \"{9}\"",
+                var segmentFormat = GetSegmentFileExtension(state.Request).TrimStart('.');
+                if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase))
+                {
+                    segmentFormat = "mpegts";
+                }
+
+                baseUrlParam = string.Format("\"{0}/\"", "hls/" + Path.GetFileNameWithoutExtension(outputPath));
+
+                return string.Format("{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -f segment -max_delay 5000000 -avoid_negative_ts disabled -start_at_zero -segment_time {6} {10} -individual_header_trailer 0 -segment_format {11} -segment_list_entry_prefix {12} -segment_list_type m3u8 -segment_start_number {7} -segment_list \"{8}\" -y \"{9}\"",
                     inputModifier,
                     EncodingHelper.GetInputArgument(state, encodingOptions),
                     threads,
@@ -276,7 +294,9 @@ namespace MediaBrowser.Api.Playback.Hls
                     startNumberParam,
                     outputPath,
                     outputTsArg,
-                    timeDeltaParam
+                    timeDeltaParam,
+                    segmentFormat,
+                    baseUrlParam
                 ).Trim();
             }
 
@@ -286,7 +306,7 @@ namespace MediaBrowser.Api.Playback.Hls
             var args = string.Format("{0} {1} {2} -map_metadata -1 -map_chapters -1 -threads {3} {4} {5} -max_delay 5000000 -avoid_negative_ts disabled -start_at_zero {6} -hls_time {7} -individual_header_trailer 0 -start_number {8} -hls_list_size {9}{10} -y \"{11}\"",
                 itsOffset,
                 inputModifier,
-                    EncodingHelper.GetInputArgument(state, encodingOptions),
+                EncodingHelper.GetInputArgument(state, encodingOptions),
                 threads,
                 EncodingHelper.GetMapArgs(state),
                 GetVideoArguments(state),

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

@@ -65,7 +65,7 @@ namespace MediaBrowser.Api.Playback.Hls
     {
     }
 
-    [Route("/Videos/{Id}/hls1/{PlaylistId}/{SegmentId}.ts", "GET")]
+    [Route("/Videos/{Id}/hls1/{PlaylistId}/{SegmentId}.{SegmentContainer}", "GET")]
     public class GetHlsVideoSegment : VideoStreamRequest
     {
         public string PlaylistId { get; set; }
@@ -77,8 +77,7 @@ namespace MediaBrowser.Api.Playback.Hls
         public string SegmentId { get; set; }
     }
 
-    [Route("/Audio/{Id}/hls1/{PlaylistId}/{SegmentId}.aac", "GET")]
-    [Route("/Audio/{Id}/hls1/{PlaylistId}/{SegmentId}.ts", "GET")]
+    [Route("/Audio/{Id}/hls1/{PlaylistId}/{SegmentId}.{SegmentContainer}", "GET")]
     public class GetHlsAudioSegment : StreamRequest
     {
         public string PlaylistId { get; set; }
@@ -158,7 +157,7 @@ namespace MediaBrowser.Api.Playback.Hls
 
             var segmentPath = GetSegmentPath(state, playlistPath, requestedIndex);
 
-            var segmentExtension = GetSegmentFileExtension(state);
+            var segmentExtension = GetSegmentFileExtension(state.Request);
 
             TranscodingJob job = null;
 
@@ -420,7 +419,7 @@ namespace MediaBrowser.Api.Playback.Hls
 
             var filename = Path.GetFileNameWithoutExtension(playlist);
 
-            return Path.Combine(folder, filename + index.ToString(UsCulture) + GetSegmentFileExtension(state));
+            return Path.Combine(folder, filename + index.ToString(UsCulture) + GetSegmentFileExtension(state.Request));
         }
 
         private async Task<object> GetSegmentResult(StreamState state, string playlistPath,
@@ -740,7 +739,7 @@ namespace MediaBrowser.Api.Playback.Hls
 
                     name,
                     index.ToString(UsCulture),
-                    GetSegmentFileExtension(isOutputVideo),
+                    GetSegmentFileExtension(request),
                     queryString));
 
                 index++;
@@ -848,7 +847,7 @@ namespace MediaBrowser.Api.Playback.Hls
 
                 var encodingOptions = ApiEntryPoint.Instance.GetEncodingOptions();
 
-                args += " " + EncodingHelper.GetVideoQualityParam(state, EncodingHelper.GetH264Encoder(state, encodingOptions), encodingOptions, GetDefaultH264Preset()) + keyFrameArg;
+                args += " " + EncodingHelper.GetVideoQualityParam(state, codec, encodingOptions, GetDefaultH264Preset()) + keyFrameArg;
 
                 //args += " -mixed-refs 0 -refs 3 -x264opts b_pyramid=0:weightb=0:weightp=0";
 
@@ -897,7 +896,7 @@ namespace MediaBrowser.Api.Playback.Hls
 
             if (useGenericSegmenter)
             {
-                var outputTsArg = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d" + GetSegmentFileExtension(state);
+                var outputTsArg = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d" + GetSegmentFileExtension(state.Request);
 
                 var timeDeltaParam = String.Empty;
 
@@ -907,7 +906,13 @@ namespace MediaBrowser.Api.Playback.Hls
                     timeDeltaParam = string.Format("-segment_time_delta -{0}", startTime);
                 }
 
-                return string.Format("{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -f segment -max_delay 5000000 -avoid_negative_ts disabled -start_at_zero -segment_time {6} {10} -individual_header_trailer 0 -segment_format mpegts -segment_list_type m3u8 -segment_start_number {7} -segment_list \"{8}\" -y \"{9}\"",
+                var segmentFormat = GetSegmentFileExtension(state.Request).TrimStart('.');
+                if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase))
+                {
+                    segmentFormat = "mpegts";
+                }
+
+                return string.Format("{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -f segment -max_delay 5000000 -avoid_negative_ts disabled -start_at_zero -segment_time {6} {10} -individual_header_trailer 0 -segment_format {11} -segment_list_type m3u8 -segment_start_number {7} -segment_list \"{8}\" -y \"{9}\"",
                     inputModifier,
                     EncodingHelper.GetInputArgument(state, encodingOptions),
                     threads,
@@ -918,7 +923,8 @@ namespace MediaBrowser.Api.Playback.Hls
                     startNumberParam,
                     outputPath,
                     outputTsArg,
-                    timeDeltaParam
+                    timeDeltaParam,
+                    segmentFormat
                 ).Trim();
             }
 
@@ -935,20 +941,5 @@ namespace MediaBrowser.Api.Playback.Hls
                             outputPath
                             ).Trim();
         }
-
-        /// <summary>
-        /// Gets the segment file extension.
-        /// </summary>
-        /// <param name="state">The state.</param>
-        /// <returns>System.String.</returns>
-        protected override string GetSegmentFileExtension(StreamState state)
-        {
-            return GetSegmentFileExtension(state.IsOutputVideo);
-        }
-
-        protected string GetSegmentFileExtension(bool isOutputVideo)
-        {
-            return isOutputVideo ? ".ts" : ".ts";
-        }
     }
 }

+ 5 - 3
MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs

@@ -63,7 +63,7 @@ namespace MediaBrowser.Api.Playback.Hls
     /// <summary>
     /// Class GetHlsVideoSegment
     /// </summary>
-    [Route("/Videos/{Id}/hls/{PlaylistId}/{SegmentId}.ts", "GET")]
+    [Route("/Videos/{Id}/hls/{PlaylistId}/{SegmentId}.{SegmentContainer}", "GET")]
     public class GetHlsVideoSegmentLegacy : VideoStreamRequest
     {
         public string PlaylistId { get; set; }
@@ -109,11 +109,13 @@ namespace MediaBrowser.Api.Playback.Hls
         public Task<object> Get(GetHlsVideoSegmentLegacy request)
         {
             var file = request.SegmentId + Path.GetExtension(Request.PathInfo);
-            file = Path.Combine(_config.ApplicationPaths.TranscodingTempPath, file);
+
+            var transcodeFolderPath = _config.ApplicationPaths.TranscodingTempPath;
+            file = Path.Combine(transcodeFolderPath, file);
 
             var normalizedPlaylistId = request.PlaylistId;
 
-            var playlistPath = _fileSystem.GetFilePaths(_config.ApplicationPaths.TranscodingTempPath)
+            var playlistPath = _fileSystem.GetFilePaths(transcodeFolderPath)
                 .FirstOrDefault(i => string.Equals(Path.GetExtension(i), ".m3u8", StringComparison.OrdinalIgnoreCase) && i.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1);
 
             return GetFileResult(file, playlistPath);

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

@@ -99,7 +99,7 @@ namespace MediaBrowser.Api.Playback.Hls
             var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.VideoRequest.SubtitleMethod == SubtitleDeliveryMethod.Encode;
 
             var encodingOptions = ApiEntryPoint.Instance.GetEncodingOptions();
-            args += " " + EncodingHelper.GetVideoQualityParam(state, EncodingHelper.GetH264Encoder(state, encodingOptions), encodingOptions, GetDefaultH264Preset()) + keyFrameArg;
+            args += " " + EncodingHelper.GetVideoQualityParam(state, codec, encodingOptions, GetDefaultH264Preset()) + keyFrameArg;
 
             // Add resolution params, if specified
             if (!hasGraphicalSubs)
@@ -118,16 +118,6 @@ namespace MediaBrowser.Api.Playback.Hls
             return args;
         }
 
-        /// <summary>
-        /// Gets the segment file extension.
-        /// </summary>
-        /// <param name="state">The state.</param>
-        /// <returns>System.String.</returns>
-        protected override string GetSegmentFileExtension(StreamState state)
-        {
-            return ".ts";
-        }
-
         public VideoHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient, IJsonSerializer jsonSerializer, IAuthorizationContext authorizationContext) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, dlnaManager, subtitleEncoder, deviceManager, mediaSourceManager, zipClient, jsonSerializer, authorizationContext)
         {
         }

+ 38 - 30
MediaBrowser.Api/Playback/MediaInfoService.cs

@@ -127,7 +127,7 @@ namespace MediaBrowser.Api.Playback
 
                 SetDeviceSpecificData(item, result.MediaSource, profile, authInfo, request.MaxStreamingBitrate,
                     request.StartTimeTicks ?? 0, result.MediaSource.Id, request.AudioStreamIndex,
-                    request.SubtitleStreamIndex, request.MaxAudioChannels, request.PlaySessionId, request.UserId, true, true, true);
+                    request.SubtitleStreamIndex, request.MaxAudioChannels, request.PlaySessionId, request.UserId, true, true, true, true);
             }
             else
             {
@@ -169,7 +169,7 @@ namespace MediaBrowser.Api.Playback
             {
                 var mediaSourceId = request.MediaSourceId;
 
-                SetDeviceSpecificData(request.Id, info, profile, authInfo, request.MaxStreamingBitrate ?? profile.MaxStreamingBitrate, request.StartTimeTicks ?? 0, mediaSourceId, request.AudioStreamIndex, request.SubtitleStreamIndex, request.MaxAudioChannels, request.UserId, request.EnableDirectPlay, request.EnableDirectStream, request.EnableTranscoding);
+                SetDeviceSpecificData(request.Id, info, profile, authInfo, request.MaxStreamingBitrate ?? profile.MaxStreamingBitrate, request.StartTimeTicks ?? 0, mediaSourceId, request.AudioStreamIndex, request.SubtitleStreamIndex, request.MaxAudioChannels, request.UserId, request.EnableDirectPlay, request.ForceDirectPlayRemoteMediaSource, request.EnableDirectStream, request.EnableTranscoding);
             }
 
             return info;
@@ -253,6 +253,7 @@ namespace MediaBrowser.Api.Playback
             int? maxAudioChannels,
             string userId,
             bool enableDirectPlay,
+            bool forceDirectPlayRemoteMediaSource,
             bool enableDirectStream,
             bool enableTranscoding)
         {
@@ -260,7 +261,7 @@ namespace MediaBrowser.Api.Playback
 
             foreach (var mediaSource in result.MediaSources)
             {
-                SetDeviceSpecificData(item, mediaSource, profile, auth, maxBitrate, startTimeTicks, mediaSourceId, audioStreamIndex, subtitleStreamIndex, maxAudioChannels, result.PlaySessionId, userId, enableDirectPlay, enableDirectStream, enableTranscoding);
+                SetDeviceSpecificData(item, mediaSource, profile, auth, maxBitrate, startTimeTicks, mediaSourceId, audioStreamIndex, subtitleStreamIndex, maxAudioChannels, result.PlaySessionId, userId, enableDirectPlay, forceDirectPlayRemoteMediaSource, enableDirectStream, enableTranscoding);
             }
 
             SortMediaSources(result, maxBitrate);
@@ -279,6 +280,7 @@ namespace MediaBrowser.Api.Playback
             string playSessionId,
             string userId,
             bool enableDirectPlay,
+            bool forceDirectPlayRemoteMediaSource,
             bool enableDirectStream,
             bool enableTranscoding)
         {
@@ -318,43 +320,49 @@ namespace MediaBrowser.Api.Playback
 
             if (mediaSource.SupportsDirectPlay)
             {
-                var supportsDirectStream = mediaSource.SupportsDirectStream;
+                if (mediaSource.IsRemote && forceDirectPlayRemoteMediaSource)
+                {
+                }
+                else
+                {
+                    var supportsDirectStream = mediaSource.SupportsDirectStream;
 
-                // Dummy this up to fool StreamBuilder
-                mediaSource.SupportsDirectStream = true;
-                options.MaxBitrate = maxBitrate;
+                    // Dummy this up to fool StreamBuilder
+                    mediaSource.SupportsDirectStream = true;
+                    options.MaxBitrate = maxBitrate;
 
-                if (item is Audio)
-                {
-                    if (!user.Policy.EnableAudioPlaybackTranscoding)
+                    if (item is Audio)
                     {
-                        options.ForceDirectPlay = true;
+                        if (!user.Policy.EnableAudioPlaybackTranscoding)
+                        {
+                            options.ForceDirectPlay = true;
+                        }
                     }
-                }
-                else if (item is Video)
-                {
-                    if (!user.Policy.EnableAudioPlaybackTranscoding && !user.Policy.EnableVideoPlaybackTranscoding && !user.Policy.EnablePlaybackRemuxing)
+                    else if (item is Video)
                     {
-                        options.ForceDirectPlay = true;
+                        if (!user.Policy.EnableAudioPlaybackTranscoding && !user.Policy.EnableVideoPlaybackTranscoding && !user.Policy.EnablePlaybackRemuxing)
+                        {
+                            options.ForceDirectPlay = true;
+                        }
                     }
-                }
 
-                // The MediaSource supports direct stream, now test to see if the client supports it
-                var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ?
-                    streamBuilder.BuildAudioItem(options) :
-                    streamBuilder.BuildVideoItem(options);
+                    // The MediaSource supports direct stream, now test to see if the client supports it
+                    var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ?
+                        streamBuilder.BuildAudioItem(options) :
+                        streamBuilder.BuildVideoItem(options);
 
-                if (streamInfo == null || !streamInfo.IsDirectStream)
-                {
-                    mediaSource.SupportsDirectPlay = false;
-                }
+                    if (streamInfo == null || !streamInfo.IsDirectStream)
+                    {
+                        mediaSource.SupportsDirectPlay = false;
+                    }
 
-                // Set this back to what it was
-                mediaSource.SupportsDirectStream = supportsDirectStream;
+                    // Set this back to what it was
+                    mediaSource.SupportsDirectStream = supportsDirectStream;
 
-                if (streamInfo != null)
-                {
-                    SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
+                    if (streamInfo != null)
+                    {
+                        SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
+                    }
                 }
             }
 

+ 2 - 4
MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs

@@ -15,9 +15,6 @@ using System.Globalization;
 using System.IO;
 using System.Threading;
 using System.Threading.Tasks;
-using MediaBrowser.Common.IO;
-using MediaBrowser.Controller.IO;
-using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Services;
 
 namespace MediaBrowser.Api.Playback.Progressive
@@ -298,7 +295,8 @@ namespace MediaBrowser.Api.Playback.Progressive
                 responseHeaders["Accept-Ranges"] = "none";
             }
 
-            if (response.ContentLength.HasValue)
+            // Seeing cases of -1 here
+            if (response.ContentLength.HasValue && response.ContentLength.Value >= 0)
             {
                 responseHeaders["Content-Length"] = response.ContentLength.Value.ToString(UsCulture);
             }

+ 1 - 183
MediaBrowser.Api/Playback/Progressive/VideoService.cs

@@ -1,4 +1,3 @@
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Dlna;
@@ -7,15 +6,8 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Serialization;
-using System;
-using System.IO;
-using System.Linq;
 using System.Threading.Tasks;
-using MediaBrowser.Common.IO;
-using MediaBrowser.Controller.IO;
 using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Services;
 
 namespace MediaBrowser.Api.Playback.Progressive
@@ -100,181 +92,7 @@ namespace MediaBrowser.Api.Playback.Progressive
         {
             var encodingOptions = ApiEntryPoint.Instance.GetEncodingOptions();
 
-            // Get the output codec name
-            var videoCodec = EncodingHelper.GetVideoEncoder(state, encodingOptions);
-
-            var format = string.Empty;
-            var keyFrame = string.Empty;
-
-            if (string.Equals(Path.GetExtension(outputPath), ".mp4", StringComparison.OrdinalIgnoreCase))
-            {
-                // Comparison: https://github.com/jansmolders86/mediacenterjs/blob/master/lib/transcoding/desktop.js
-                format = " -f mp4 -movflags frag_keyframe+empty_moov";
-            }
-
-            var threads = EncodingHelper.GetNumberOfThreads(state, encodingOptions, string.Equals(videoCodec, "libvpx", StringComparison.OrdinalIgnoreCase));
-
-            var inputModifier = EncodingHelper.GetInputModifier(state, encodingOptions);
-
-            var subtitleArguments = state.SubtitleStream != null &&
-                                    state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Embed
-                ? GetSubtitleArguments(state)
-                : string.Empty;
-
-            return string.Format("{0} {1}{2} {3} {4} -map_metadata -1 -map_chapters -1 -threads {5} {6}{7}{8} -y \"{9}\"",
-                inputModifier,
-                EncodingHelper.GetInputArgument(state, encodingOptions),
-                keyFrame,
-                EncodingHelper.GetMapArgs(state),
-                GetVideoArguments(state, videoCodec),
-                threads,
-                GetAudioArguments(state),
-                subtitleArguments,
-                format,
-                outputPath
-                ).Trim();
-        }
-
-        private string GetSubtitleArguments(StreamState state)
-        {
-            var format = state.SupportedSubtitleCodecs.FirstOrDefault();
-            string codec;
-
-            if (string.IsNullOrWhiteSpace(format) || string.Equals(format, state.SubtitleStream.Codec, StringComparison.OrdinalIgnoreCase))
-            {
-                codec = "copy";
-            }
-            else
-            {
-                codec = format;
-            }
-
-            return " -codec:s:0 " + codec;
-        }
-
-        /// <summary>
-        /// Gets video arguments to pass to ffmpeg
-        /// </summary>
-        /// <param name="state">The state.</param>
-        /// <param name="videoCodec">The video codec.</param>
-        /// <returns>System.String.</returns>
-        private string GetVideoArguments(StreamState state, string videoCodec)
-        {
-            var args = "-codec:v:0 " + videoCodec;
-
-            if (state.EnableMpegtsM2TsMode)
-            {
-                args += " -mpegts_m2ts_mode 1";
-            }
-
-            if (string.Equals(videoCodec, "copy", StringComparison.OrdinalIgnoreCase))
-            {
-                if (state.VideoStream != null && EncodingHelper.IsH264(state.VideoStream) && string.Equals(state.OutputContainer, "ts", StringComparison.OrdinalIgnoreCase) && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
-                {
-                    args += " -bsf:v h264_mp4toannexb";
-                }
-
-                if (state.RunTimeTicks.HasValue && state.VideoRequest.CopyTimestamps)
-                {
-                    args += " -copyts -avoid_negative_ts disabled -start_at_zero";
-                }
-
-                if (!state.RunTimeTicks.HasValue)
-                {
-                    args += " -flags -global_header -fflags +genpts";
-                }
-
-                return args;
-            }
-
-            var keyFrameArg = string.Format(" -force_key_frames \"expr:gte(t,n_forced*{0})\"",
-                5.ToString(UsCulture));
-
-            args += keyFrameArg;
-
-            var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.VideoRequest.SubtitleMethod == SubtitleDeliveryMethod.Encode;
-
-            var hasCopyTs = false;
-            // Add resolution params, if specified
-            if (!hasGraphicalSubs)
-            {
-                var outputSizeParam = EncodingHelper.GetOutputSizeParam(state, videoCodec);
-                args += outputSizeParam;
-                hasCopyTs = outputSizeParam.IndexOf("copyts", StringComparison.OrdinalIgnoreCase) != -1;
-            }
-
-            if (state.RunTimeTicks.HasValue && state.VideoRequest.CopyTimestamps)
-            {
-                if (!hasCopyTs)
-                {
-                    args += " -copyts";
-                }
-                args += " -avoid_negative_ts disabled -start_at_zero";
-            }
-
-            var encodingOptions = ApiEntryPoint.Instance.GetEncodingOptions();
-            var qualityParam = EncodingHelper.GetVideoQualityParam(state, videoCodec, encodingOptions, GetDefaultH264Preset());
-
-            if (!string.IsNullOrEmpty(qualityParam))
-            {
-                args += " " + qualityParam.Trim();
-            }
-
-            // This is for internal graphical subs
-            if (hasGraphicalSubs)
-            {
-                args += EncodingHelper.GetGraphicalSubtitleParam(state, videoCodec);
-            }
-
-            if (!state.RunTimeTicks.HasValue)
-            {
-                args += " -flags -global_header";
-            }
-
-            return args;
-        }
-
-        /// <summary>
-        /// Gets audio arguments to pass to ffmpeg
-        /// </summary>
-        /// <param name="state">The state.</param>
-        /// <returns>System.String.</returns>
-        private string GetAudioArguments(StreamState state)
-        {
-            // If the video doesn't have an audio stream, return a default.
-            if (state.AudioStream == null && state.VideoStream != null)
-            {
-                return string.Empty;
-            }
-
-            // Get the output codec name
-            var codec = EncodingHelper.GetAudioEncoder(state);
-
-            var args = "-codec:a:0 " + codec;
-
-            if (string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase))
-            {
-                return args;
-            }
-
-            // Add the number of audio channels
-            var channels = state.OutputAudioChannels;
-
-            if (channels.HasValue)
-            {
-                args += " -ac " + channels.Value;
-            }
-
-            var bitrate = state.OutputAudioBitrate;
-
-            if (bitrate.HasValue)
-            {
-                args += " -ab " + bitrate.Value.ToString(UsCulture);
-            }
-
-            args += " " + EncodingHelper.GetAudioFilterParam(state, ApiEntryPoint.Instance.GetEncodingOptions(), false);
-
-            return args;
+            return EncodingHelper.GetProgressiveVideoFullCommandLine(state, encodingOptions, outputPath, GetDefaultH264Preset());
         }
     }
 }

+ 4 - 2
MediaBrowser.Api/Playback/StreamRequest.cs

@@ -32,8 +32,6 @@ namespace MediaBrowser.Api.Playback
         [ApiMember(Name = "AudioCodec", Description = "Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
         public string AudioCodec { get; set; }
 
-        public string SubtitleCodec { get; set; }
-
         [ApiMember(Name = "DeviceProfileId", Description = "Optional. The dlna device profile id to utilize.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
         public string DeviceProfileId { get; set; }
 
@@ -41,6 +39,10 @@ namespace MediaBrowser.Api.Playback
         public string PlaySessionId { get; set; }
         public string LiveStreamId { get; set; }
         public string Tag { get; set; }
+        public string SegmentContainer { get; set; }
+
+        public int? SegmentLength { get; set; }
+        public int? MinSegments { get; set; }
     }
 
     public class VideoStreamRequest : StreamRequest

+ 26 - 4
MediaBrowser.Api/Playback/StreamState.cs

@@ -60,6 +60,11 @@ namespace MediaBrowser.Api.Playback
         {
             get
             {
+                if (Request.SegmentLength.HasValue)
+                {
+                    return Request.SegmentLength.Value;
+                }
+
                 if (string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
                 {
                     var userAgent = UserAgent ?? string.Empty;
@@ -86,6 +91,19 @@ namespace MediaBrowser.Api.Playback
             }
         }
 
+        public int MinSegments
+        {
+            get
+            {
+                if (Request.MinSegments.HasValue)
+                {
+                    return Request.MinSegments.Value;
+                }
+
+                return SegmentLength >= 10 ? 2 : 3;
+            }
+        }
+
         public bool IsSegmentedLiveStream
         {
             get
@@ -102,7 +120,6 @@ namespace MediaBrowser.Api.Playback
             }
         }
 
-        public List<string> SupportedSubtitleCodecs { get; set; }
         public string UserAgent { get; set; }
         public TranscodingJobType TranscodingType { get; set; }
 
@@ -111,14 +128,12 @@ namespace MediaBrowser.Api.Playback
         {
             _mediaSourceManager = mediaSourceManager;
             _logger = logger;
-            SupportedSubtitleCodecs = new List<string>();
             TranscodingType = transcodingType;
         }
 
         public string MimeType { get; set; }
 
         public bool EstimateContentLength { get; set; }
-        public bool EnableMpegtsM2TsMode { get; set; }
         public TranscodeSeekInfo TranscodeSeekInfo { get; set; }
 
         public long? EncodingDurationTicks { get; set; }
@@ -139,6 +154,8 @@ namespace MediaBrowser.Api.Playback
             DisposeLiveStream();
             DisposeLogStream();
             DisposeIsoMount();
+
+            TranscodingJob = null;
         }
 
         private void DisposeLogStream()
@@ -191,7 +208,6 @@ namespace MediaBrowser.Api.Playback
         }
 
         public string OutputFilePath { get; set; }
-        public int? OutputAudioBitrate;
 
         public string ActualOutputVideoCodec
         {
@@ -462,5 +478,11 @@ namespace MediaBrowser.Api.Playback
                 return true;
             }
         }
+
+        public TranscodingJob TranscodingJob;
+        public override void ReportTranscodingProgress(TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded, int? bitRate)
+        {
+            ApiEntryPoint.Instance.ReportTranscodingProgress(TranscodingJob, this, transcodingPosition, framerate, percentComplete, bytesTranscoded, bitRate);
+        }
     }
 }

+ 3 - 73
MediaBrowser.Api/StartupWizardService.cs

@@ -1,12 +1,9 @@
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Controller;
+using MediaBrowser.Controller;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Connect;
 using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.LiveTv;
 using System;
 using System.Linq;
 using System.Threading.Tasks;
@@ -52,16 +49,14 @@ namespace MediaBrowser.Api
         private readonly IServerApplicationHost _appHost;
         private readonly IUserManager _userManager;
         private readonly IConnectManager _connectManager;
-        private readonly ILiveTvManager _liveTvManager;
         private readonly IMediaEncoder _mediaEncoder;
 
-        public StartupWizardService(IServerConfigurationManager config, IServerApplicationHost appHost, IUserManager userManager, IConnectManager connectManager, ILiveTvManager liveTvManager, IMediaEncoder mediaEncoder)
+        public StartupWizardService(IServerConfigurationManager config, IServerApplicationHost appHost, IUserManager userManager, IConnectManager connectManager, IMediaEncoder mediaEncoder)
         {
             _config = config;
             _appHost = appHost;
             _userManager = userManager;
             _connectManager = connectManager;
-            _liveTvManager = liveTvManager;
             _mediaEncoder = mediaEncoder;
         }
 
@@ -92,20 +87,6 @@ namespace MediaBrowser.Api
                 PreferredMetadataLanguage = _config.Configuration.PreferredMetadataLanguage
             };
 
-            var tvConfig = GetLiveTVConfiguration();
-
-            if (tvConfig.TunerHosts.Count > 0)
-            {
-                result.LiveTvTunerPath = tvConfig.TunerHosts[0].Url;
-                result.LiveTvTunerType = tvConfig.TunerHosts[0].Type;
-            }
-
-            if (tvConfig.ListingProviders.Count > 0)
-            {
-                result.LiveTvGuideProviderId = tvConfig.ListingProviders[0].Id;
-                result.LiveTvGuideProviderType = tvConfig.ListingProviders[0].Type;
-            }
-
             return result;
         }
 
@@ -120,6 +101,7 @@ namespace MediaBrowser.Api
             config.EnableSeriesPresentationUniqueKey = true;
             config.EnableLocalizedGuids = true;
             config.EnableSimpleArtistDetection = true;
+            config.EnableNormalizedItemByNameIds = true;
         }
 
         public void Post(UpdateStartupConfiguration request)
@@ -128,9 +110,6 @@ namespace MediaBrowser.Api
             _config.Configuration.MetadataCountryCode = request.MetadataCountryCode;
             _config.Configuration.PreferredMetadataLanguage = request.PreferredMetadataLanguage;
             _config.SaveConfiguration();
-
-            var task = UpdateTuners(request);
-            Task.WaitAll(task);
         }
 
         public object Get(GetStartupUser request)
@@ -165,51 +144,6 @@ namespace MediaBrowser.Api
 
             return result;
         }
-
-        private async Task UpdateTuners(UpdateStartupConfiguration request)
-        {
-            var config = GetLiveTVConfiguration();
-            var save = false;
-
-            if (string.IsNullOrWhiteSpace(request.LiveTvTunerPath) ||
-                string.IsNullOrWhiteSpace(request.LiveTvTunerType))
-            {
-                if (config.TunerHosts.Count > 0)
-                {
-                    config.TunerHosts.Clear();
-                    save = true;
-                }
-            }
-            else
-            {
-                if (!config.TunerHosts.Any(i => string.Equals(i.Type, request.LiveTvTunerType, StringComparison.OrdinalIgnoreCase) && string.Equals(i.Url, request.LiveTvTunerPath, StringComparison.OrdinalIgnoreCase)))
-                {
-                    // Add tuner
-                    await _liveTvManager.SaveTunerHost(new TunerHostInfo
-                    {
-                        IsEnabled = true,
-                        Type = request.LiveTvTunerType,
-                        Url = request.LiveTvTunerPath
-
-                    }).ConfigureAwait(false);
-                }
-            }
-
-            if (save)
-            {
-                SaveLiveTVConfiguration(config);
-            }
-        }
-
-        private void SaveLiveTVConfiguration(LiveTvOptions config)
-        {
-            _config.SaveConfiguration("livetv", config);
-        }
-
-        private LiveTvOptions GetLiveTVConfiguration()
-        {
-            return _config.GetConfiguration<LiveTvOptions>("livetv");
-        }
     }
 
     public class StartupConfiguration
@@ -217,10 +151,6 @@ namespace MediaBrowser.Api
         public string UICulture { get; set; }
         public string MetadataCountryCode { get; set; }
         public string PreferredMetadataLanguage { get; set; }
-        public string LiveTvTunerType { get; set; }
-        public string LiveTvTunerPath { get; set; }
-        public string LiveTvGuideProviderId { get; set; }
-        public string LiveTvGuideProviderType { get; set; }
     }
 
     public class StartupInfo

+ 8 - 1
MediaBrowser.Api/TvShowsService.cs

@@ -72,6 +72,12 @@ namespace MediaBrowser.Api
 
         [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
         public bool? EnableUserData { get; set; }
+        public bool EnableTotalRecordCount { get; set; }
+
+        public GetNextUpEpisodes()
+        {
+            EnableTotalRecordCount = true;
+        }
     }
 
     [Route("/Shows/Upcoming", "GET", Summary = "Gets a list of upcoming episodes")]
@@ -376,7 +382,8 @@ namespace MediaBrowser.Api
                 ParentId = request.ParentId,
                 SeriesId = request.SeriesId,
                 StartIndex = request.StartIndex,
-                UserId = request.UserId
+                UserId = request.UserId,
+                EnableTotalRecordCount = request.EnableTotalRecordCount
             });
 
             var user = _userManager.GetUserById(request.UserId);

+ 6 - 1
MediaBrowser.Controller/Entities/Audio/MusicArtist.cs

@@ -289,7 +289,12 @@ namespace MediaBrowser.Controller.Entities.Audio
             }
         }
 
-        public static string GetPath(string name, bool normalizeName = true)
+        public static string GetPath(string name)
+        {
+            return GetPath(name, true);
+        }
+
+        public static string GetPath(string name, bool normalizeName)
         {
             // Trim the period at the end because windows will have a hard time with that
             var validName = normalizeName ?

+ 6 - 1
MediaBrowser.Controller/Entities/Audio/MusicGenre.cs

@@ -118,7 +118,12 @@ namespace MediaBrowser.Controller.Entities.Audio
             return LibraryManager.GetItemList(query);
         }
 
-        public static string GetPath(string name, bool normalizeName = true)
+        public static string GetPath(string name)
+        {
+            return GetPath(name, true);
+        }
+
+        public static string GetPath(string name, bool normalizeName)
         {
             // Trim the period at the end because windows will have a hard time with that
             var validName = normalizeName ?

+ 6 - 1
MediaBrowser.Controller/Entities/GameGenre.cs

@@ -96,7 +96,12 @@ namespace MediaBrowser.Controller.Entities
             }
         }
 
-        public static string GetPath(string name, bool normalizeName = true)
+        public static string GetPath(string name)
+        {
+            return GetPath(name, true);
+        }
+
+        public static string GetPath(string name, bool normalizeName)
         {
             // Trim the period at the end because windows will have a hard time with that
             var validName = normalizeName ?

+ 6 - 1
MediaBrowser.Controller/Entities/Genre.cs

@@ -108,7 +108,12 @@ namespace MediaBrowser.Controller.Entities
             }
         }
 
-        public static string GetPath(string name, bool normalizeName = true)
+        public static string GetPath(string name)
+        {
+            return GetPath(name, true);
+        }
+
+        public static string GetPath(string name, bool normalizeName)
         {
             // Trim the period at the end because windows will have a hard time with that
             var validName = normalizeName ?

+ 2 - 0
MediaBrowser.Controller/Entities/IHasImages.cs

@@ -206,6 +206,8 @@ namespace MediaBrowser.Controller.Entities
         void SetImage(ItemImageInfo image, int index);
 
         double? GetDefaultPrimaryImageAspectRatio();
+
+        int? ProductionYear { get; set; }
     }
 
     public static class HasImagesExtensions

+ 6 - 1
MediaBrowser.Controller/Entities/Person.cs

@@ -133,7 +133,12 @@ namespace MediaBrowser.Controller.Entities
             }
         }
 
-        public static string GetPath(string name, bool normalizeName = true)
+        public static string GetPath(string name)
+        {
+            return GetPath(name, true);
+        }
+
+        public static string GetPath(string name, bool normalizeName)
         {
             // Trim the period at the end because windows will have a hard time with that
             var validFilename = normalizeName ?

+ 6 - 1
MediaBrowser.Controller/Entities/Studio.cs

@@ -114,7 +114,12 @@ namespace MediaBrowser.Controller.Entities
             }
         }
 
-        public static string GetPath(string name, bool normalizeName = true)
+        public static string GetPath(string name)
+        {
+            return GetPath(name, true);
+        }
+
+        public static string GetPath(string name, bool normalizeName)
         {
             // Trim the period at the end because windows will have a hard time with that
             var validName = normalizeName ?

+ 2 - 1
MediaBrowser.Controller/Entities/Video.cs

@@ -614,7 +614,8 @@ namespace MediaBrowser.Controller.Entities
                 Timestamp = i.Timestamp,
                 Type = type,
                 PlayableStreamFileNames = i.PlayableStreamFileNames.ToList(),
-                SupportsDirectStream = i.VideoType == VideoType.VideoFile
+                SupportsDirectStream = i.VideoType == VideoType.VideoFile,
+                IsRemote = i.IsShortcut
             };
 
             if (info.Protocol == MediaProtocol.File)

+ 6 - 1
MediaBrowser.Controller/Entities/Year.cs

@@ -122,7 +122,12 @@ namespace MediaBrowser.Controller.Entities
             }
         }
 
-        public static string GetPath(string name, bool normalizeName = true)
+        public static string GetPath(string name)
+        {
+            return GetPath(name, true);
+        }
+
+        public static string GetPath(string name, bool normalizeName)
         {
             // Trim the period at the end because windows will have a hard time with that
             var validName = normalizeName ?

+ 3 - 8
MediaBrowser.Controller/LiveTv/ILiveTvManager.cs

@@ -376,19 +376,14 @@ namespace MediaBrowser.Controller.LiveTv
         /// <returns>Task.</returns>
         Task OnRecordingFileDeleted(BaseItem recording);
 
-        /// <summary>
-        /// Gets the sat ini mappings.
-        /// </summary>
-        /// <returns>List&lt;NameValuePair&gt;.</returns>
-        List<NameValuePair> GetSatIniMappings();
-
-        Task<List<ChannelInfo>> GetSatChannelScanResult(TunerHostInfo info, CancellationToken cancellationToken);
-
         Task<List<ChannelInfo>> GetChannelsForListingsProvider(string id, CancellationToken cancellationToken);
         Task<List<ChannelInfo>> GetChannelsFromListingsProviderData(string id, CancellationToken cancellationToken);
 
         List<IListingsProvider> ListingProviders { get; }
 
+        List<NameIdPair> GetTunerHostTypes();
+        Task<List<TunerHostInfo>> DiscoverTuners(bool newDevicesOnly, CancellationToken cancellationToken);
+
         event EventHandler<GenericEventArgs<TimerEventInfo>> SeriesTimerCancelled;
         event EventHandler<GenericEventArgs<TimerEventInfo>> TimerCancelled;
         event EventHandler<GenericEventArgs<TimerEventInfo>> TimerCreated;

+ 2 - 0
MediaBrowser.Controller/LiveTv/ITunerHost.cs

@@ -44,6 +44,8 @@ namespace MediaBrowser.Controller.LiveTv
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task&lt;List&lt;MediaSourceInfo&gt;&gt;.</returns>
         Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(string channelId, CancellationToken cancellationToken);
+
+        Task<List<TunerHostInfo>> DiscoverDevices(int discoveryDurationMs, CancellationToken cancellationToken);
     }
     public interface IConfigurableTunerHost
     {

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

@@ -190,6 +190,7 @@
     <Compile Include="MediaEncoding\ImageEncodingOptions.cs" />
     <Compile Include="MediaEncoding\IMediaEncoder.cs" />
     <Compile Include="MediaEncoding\ISubtitleEncoder.cs" />
+    <Compile Include="MediaEncoding\JobLogger.cs" />
     <Compile Include="MediaEncoding\MediaInfoRequest.cs" />
     <Compile Include="MediaEncoding\MediaStreamSelector.cs" />
     <Compile Include="Net\AuthenticatedAttribute.cs" />

+ 233 - 4
MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs

@@ -154,6 +154,7 @@ namespace MediaBrowser.Controller.MediaEncoding
             {
                 return "mpegts";
             }
+
             // For these need to find out the ffmpeg names
             if (string.Equals(container, "m2ts", StringComparison.OrdinalIgnoreCase))
             {
@@ -163,6 +164,10 @@ namespace MediaBrowser.Controller.MediaEncoding
             {
                 return null;
             }
+            if (string.Equals(container, "mts", StringComparison.OrdinalIgnoreCase))
+            {
+                return null;
+            }
             if (string.Equals(container, "vob", StringComparison.OrdinalIgnoreCase))
             {
                 return null;
@@ -179,12 +184,23 @@ namespace MediaBrowser.Controller.MediaEncoding
             {
                 return null;
             }
+            if (string.Equals(container, "dvr-ms", StringComparison.OrdinalIgnoreCase))
+            {
+                return null;
+            }
+
             // Seeing reported failures here, not sure yet if this is related to specfying input format
             if (string.Equals(container, "m4v", StringComparison.OrdinalIgnoreCase))
             {
                 return null;
             }
 
+            // obviously don't do this for strm files
+            if (string.Equals(container, "strm", StringComparison.OrdinalIgnoreCase))
+            {
+                return null;
+            }
+
             return container;
         }
 
@@ -661,9 +677,8 @@ namespace MediaBrowser.Controller.MediaEncoding
                 var level = NormalizeTranscodingLevel(state.OutputVideoCodec, request.Level);
 
                 // h264_qsv and h264_nvenc expect levels to be expressed as a decimal. libx264 supports decimal and non-decimal format
-                // also needed for libx264 due to https://trac.ffmpeg.org/ticket/3307
+                // also needed for libx264 due to https://trac.ffmpeg.org/ticket/3307                
                 if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) ||
-                    string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) ||
                     string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase))
                 {
                     switch (level)
@@ -700,10 +715,15 @@ namespace MediaBrowser.Controller.MediaEncoding
                             break;
                     }
                 }
+                // nvenc doesn't decode with param -level set ?!
+                if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)){
+                    param += "";
+                }
                 else if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase))
                 {
                     param += " -level " + level;
                 }
+                
             }
 
             if (string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase))
@@ -727,12 +747,18 @@ namespace MediaBrowser.Controller.MediaEncoding
 
             if (videoStream.IsInterlaced)
             {
-                return false;
+                if (request.DeInterlace)
+                {
+                    return false;
+                }
             }
 
             if (videoStream.IsAnamorphic ?? false)
             {
-                return false;
+                if (request.RequireNonAnamorphic)
+                {
+                    return false;
+                }
             }
 
             // Can't stream copy if we're burning in subtitles
@@ -1561,6 +1587,15 @@ namespace MediaBrowser.Controller.MediaEncoding
           MediaSourceInfo mediaSource,
           string requestedUrl)
         {
+            if (state == null)
+            {
+                throw new ArgumentNullException("state");
+            }
+            if (mediaSource == null)
+            {
+                throw new ArgumentNullException("mediaSource");
+            }
+
             state.MediaPath = mediaSource.Path;
             state.InputProtocol = mediaSource.Protocol;
             state.InputContainer = mediaSource.Container;
@@ -1670,9 +1705,21 @@ namespace MediaBrowser.Controller.MediaEncoding
                         case "h264":
                             if (_mediaEncoder.SupportsDecoder("h264_qsv"))
                             {
+                                // qsv decoder does not support 10-bit input
+                                if ((state.VideoStream.BitDepth ?? 8) > 8)
+                                {
+                                    return null;
+                                }
                                 return "-c:v h264_qsv ";
                             }
                             break;
+                        //case "hevc":
+                        //case "h265":
+                        //    if (_mediaEncoder.SupportsDecoder("hevc_qsv"))
+                        //    {
+                        //        return "-c:v hevc_qsv ";
+                        //    }
+                        //    break;
                         case "mpeg2video":
                             if (_mediaEncoder.SupportsDecoder("mpeg2_qsv"))
                             {
@@ -1715,5 +1762,187 @@ namespace MediaBrowser.Controller.MediaEncoding
 
             return threads;
         }
+
+        public string GetSubtitleEmbedArguments(EncodingJobInfo state)
+        {
+            if (state.SubtitleStream == null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Embed)
+            {
+                return string.Empty;
+            }
+
+            var format = state.SupportedSubtitleCodecs.FirstOrDefault();
+            string codec;
+
+            if (string.IsNullOrWhiteSpace(format) || string.Equals(format, state.SubtitleStream.Codec, StringComparison.OrdinalIgnoreCase))
+            {
+                codec = "copy";
+            }
+            else
+            {
+                codec = format;
+            }
+
+            // Muxing in dvbsub via either copy or -codec dvbsub does not seem to work
+            // It doesn't throw any errors but vlc on android will not render them
+            // They will need to be converted to an alternative format
+            // TODO: This is incorrectly assuming that dvdsub will be supported by the player
+            // The api will need to be expanded to accomodate this.
+            if (string.Equals(state.SubtitleStream.Codec, "DVBSUB", StringComparison.OrdinalIgnoreCase))
+            {
+                codec = "dvdsub";
+            }
+
+            var args = " -codec:s:0 " + codec;
+
+            args += " -disposition:s:0 default";
+
+            return args;
+        }
+
+        public string GetProgressiveVideoFullCommandLine(EncodingJobInfo state, EncodingOptions encodingOptions, string outputPath, string defaultH264Preset)
+        {
+            // Get the output codec name
+            var videoCodec = GetVideoEncoder(state, encodingOptions);
+
+            var format = string.Empty;
+            var keyFrame = string.Empty;
+
+            if (string.Equals(Path.GetExtension(outputPath), ".mp4", StringComparison.OrdinalIgnoreCase) &&
+                state.BaseRequest.Context == EncodingContext.Streaming)
+            {
+                // Comparison: https://github.com/jansmolders86/mediacenterjs/blob/master/lib/transcoding/desktop.js
+                format = " -f mp4 -movflags frag_keyframe+empty_moov";
+            }
+
+            var threads = GetNumberOfThreads(state, encodingOptions, string.Equals(videoCodec, "libvpx", StringComparison.OrdinalIgnoreCase));
+
+            var inputModifier = GetInputModifier(state, encodingOptions);
+
+            return string.Format("{0} {1}{2} {3} {4} -map_metadata -1 -map_chapters -1 -threads {5} {6}{7}{8} -y \"{9}\"",
+                inputModifier,
+                GetInputArgument(state, encodingOptions),
+                keyFrame,
+                GetMapArgs(state),
+                GetProgressiveVideoArguments(state, encodingOptions, videoCodec, defaultH264Preset),
+                threads,
+                GetProgressiveVideoAudioArguments(state, encodingOptions),
+                GetSubtitleEmbedArguments(state),
+                format,
+                outputPath
+                ).Trim();
+        }
+
+        public string GetProgressiveVideoArguments(EncodingJobInfo state, EncodingOptions encodingOptions, string videoCodec, string defaultH264Preset)
+        {
+            var args = "-codec:v:0 " + videoCodec;
+
+            if (state.EnableMpegtsM2TsMode)
+            {
+                args += " -mpegts_m2ts_mode 1";
+            }
+
+            if (string.Equals(videoCodec, "copy", StringComparison.OrdinalIgnoreCase))
+            {
+                if (state.VideoStream != null && IsH264(state.VideoStream) && string.Equals(state.OutputContainer, "ts", StringComparison.OrdinalIgnoreCase) && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
+                {
+                    args += " -bsf:v h264_mp4toannexb";
+                }
+
+                if (state.RunTimeTicks.HasValue && state.BaseRequest.CopyTimestamps)
+                {
+                    args += " -copyts -avoid_negative_ts disabled -start_at_zero";
+                }
+
+                if (!state.RunTimeTicks.HasValue)
+                {
+                    args += " -flags -global_header -fflags +genpts";
+                }
+
+                return args;
+            }
+
+            var keyFrameArg = string.Format(" -force_key_frames \"expr:gte(t,n_forced*{0})\"",
+                5.ToString(_usCulture));
+
+            args += keyFrameArg;
+
+            var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.BaseRequest.SubtitleMethod == SubtitleDeliveryMethod.Encode;
+
+            var hasCopyTs = false;
+            // Add resolution params, if specified
+            if (!hasGraphicalSubs)
+            {
+                var outputSizeParam = GetOutputSizeParam(state, videoCodec);
+                args += outputSizeParam;
+                hasCopyTs = outputSizeParam.IndexOf("copyts", StringComparison.OrdinalIgnoreCase) != -1;
+            }
+
+            if (state.RunTimeTicks.HasValue && state.BaseRequest.CopyTimestamps)
+            {
+                if (!hasCopyTs)
+                {
+                    args += " -copyts";
+                }
+                args += " -avoid_negative_ts disabled -start_at_zero";
+            }
+
+            var qualityParam = GetVideoQualityParam(state, videoCodec, encodingOptions, defaultH264Preset);
+
+            if (!string.IsNullOrEmpty(qualityParam))
+            {
+                args += " " + qualityParam.Trim();
+            }
+
+            // This is for internal graphical subs
+            if (hasGraphicalSubs)
+            {
+                args += GetGraphicalSubtitleParam(state, videoCodec);
+            }
+
+            if (!state.RunTimeTicks.HasValue)
+            {
+                args += " -flags -global_header";
+            }
+
+            return args;
+        }
+
+        public string GetProgressiveVideoAudioArguments(EncodingJobInfo state, EncodingOptions encodingOptions)
+        {
+            // If the video doesn't have an audio stream, return a default.
+            if (state.AudioStream == null && state.VideoStream != null)
+            {
+                return string.Empty;
+            }
+
+            // Get the output codec name
+            var codec = GetAudioEncoder(state);
+
+            var args = "-codec:a:0 " + codec;
+
+            if (string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase))
+            {
+                return args;
+            }
+
+            // Add the number of audio channels
+            var channels = state.OutputAudioChannels;
+
+            if (channels.HasValue)
+            {
+                args += " -ac " + channels.Value;
+            }
+
+            var bitrate = state.OutputAudioBitrate;
+
+            if (bitrate.HasValue)
+            {
+                args += " -ab " + bitrate.Value.ToString(_usCulture);
+            }
+            
+            args += " " + GetAudioFilterParam(state, encodingOptions, false);
+
+            return args;
+        }
     }
 }

+ 9 - 2
MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs

@@ -12,7 +12,7 @@ using MediaBrowser.Model.MediaInfo;
 namespace MediaBrowser.Controller.MediaEncoding
 {
     // For now, a common base class until the API and MediaEncoding classes are unified
-    public class EncodingJobInfo
+    public abstract class EncodingJobInfo
     {
         private readonly ILogger _logger;
 
@@ -29,6 +29,7 @@ namespace MediaBrowser.Controller.MediaEncoding
         public int? OutputVideoBitrate { get; set; }
         public MediaStream SubtitleStream { get; set; }
         public SubtitleDeliveryMethod SubtitleDeliveryMethod { get; set; }
+        public List<string> SupportedSubtitleCodecs { get; set; }
 
         public int InternalSubtitleStreamOffset { get; set; }
         public MediaSourceInfo MediaSource { get; set; }
@@ -52,6 +53,8 @@ namespace MediaBrowser.Controller.MediaEncoding
         public string InputContainer { get; set; }
         public IsoType? IsoType { get; set; }
 
+        public bool EnableMpegtsM2TsMode { get; set; }
+
         public BaseEncodingJobOptions BaseRequest { get; set; }
 
         public long? StartTimeTicks
@@ -64,6 +67,7 @@ namespace MediaBrowser.Controller.MediaEncoding
             get { return BaseRequest.CopyTimestamps; }
         }
 
+        public int? OutputAudioBitrate;
         public int? OutputAudioChannels;
         public int? OutputAudioSampleRate;
         public bool DeInterlace { get; set; }
@@ -74,8 +78,9 @@ namespace MediaBrowser.Controller.MediaEncoding
             _logger = logger;
             RemoteHttpHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
             PlayableStreamFileNames = new List<string>();
+            SupportedAudioCodecs = new List<string>();
             SupportedVideoCodecs = new List<string>();
-            SupportedVideoCodecs = new List<string>();
+            SupportedSubtitleCodecs = new List<string>();
         }
 
         /// <summary>
@@ -110,5 +115,7 @@ namespace MediaBrowser.Controller.MediaEncoding
                 IsoMount = null;
             }
         }
+
+        public abstract void ReportTranscodingProgress(TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded, int? bitRate);
     }
 }

+ 8 - 2
MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs

@@ -14,7 +14,6 @@ namespace MediaBrowser.Controller.MediaEncoding
         public string AudioCodec { get; set; }
 
         public DeviceProfile DeviceProfile { get; set; }
-        public EncodingContext Context { get; set; }
 
         public bool ReadInputAtNativeFramerate { get; set; }
 
@@ -46,7 +45,7 @@ namespace MediaBrowser.Controller.MediaEncoding
             AudioBitRate = info.AudioBitrate;
             AudioSampleRate = info.TargetAudioSampleRate;
             DeviceProfile = deviceProfile;
-            VideoCodec = info.VideoCodec;
+            VideoCodec = info.TargetVideoCodec;
             VideoBitRate = info.VideoBitrate;
             AudioStreamIndex = info.AudioStreamIndex;
             MaxRefFrames = info.MaxRefFrames;
@@ -185,6 +184,8 @@ namespace MediaBrowser.Controller.MediaEncoding
         [ApiMember(Name = "MaxVideoBitDepth", Description = "Optional.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
         public int? MaxVideoBitDepth { get; set; }
         public bool RequireAvc { get; set; }
+        public bool DeInterlace { get; set; }
+        public bool RequireNonAnamorphic { get; set; }
         public int? TranscodingMaxAudioChannels { get; set; }
         public int? CpuCoreLimit { get; set; }
         public string OutputContainer { get; set; }
@@ -196,6 +197,8 @@ namespace MediaBrowser.Controller.MediaEncoding
         [ApiMember(Name = "VideoCodec", Description = "Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h264, mpeg4, theora, vpx, wmv.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
         public string VideoCodec { get; set; }
 
+        public string SubtitleCodec { get; set; }
+
         /// <summary>
         /// Gets or sets the index of the audio stream.
         /// </summary>
@@ -210,9 +213,12 @@ namespace MediaBrowser.Controller.MediaEncoding
         [ApiMember(Name = "VideoStreamIndex", Description = "Optional. The index of the video stream to use. If omitted the first video stream will be used.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
         public int? VideoStreamIndex { get; set; }
 
+        public EncodingContext Context { get; set; }
+
         public BaseEncodingJobOptions()
         {
             EnableAutoStreamCopy = true;
+            Context = EncodingContext.Streaming;
         }
     }
 }

+ 37 - 10
MediaBrowser.MediaEncoding/Encoder/JobLogger.cs → MediaBrowser.Controller/MediaEncoding/JobLogger.cs

@@ -6,7 +6,7 @@ using System.IO;
 using System.Linq;
 using System.Text;
 
-namespace MediaBrowser.MediaEncoding.Encoder
+namespace MediaBrowser.Controller.MediaEncoding
 {
     public class JobLogger
     {
@@ -18,7 +18,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
             _logger = logger;
         }
 
-        public async void StartStreamingLog(EncodingJob transcodingJob, Stream source, Stream target)
+        public async void StartStreamingLog(EncodingJobInfo state, Stream source, Stream target)
         {
             try
             {
@@ -28,35 +28,41 @@ namespace MediaBrowser.MediaEncoding.Encoder
                     {
                         var line = await reader.ReadLineAsync().ConfigureAwait(false);
 
-                        ParseLogLine(line, transcodingJob);
+                        ParseLogLine(line, state);
 
                         var bytes = Encoding.UTF8.GetBytes(Environment.NewLine + line);
 
                         await target.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
+                        await target.FlushAsync().ConfigureAwait(false);
                     }
                 }
             }
+            catch (ObjectDisposedException)
+            {
+                // Don't spam the log. This doesn't seem to throw in windows, but sometimes under linux
+            }
             catch (Exception ex)
             {
                 _logger.ErrorException("Error reading ffmpeg log", ex);
             }
         }
 
-        private void ParseLogLine(string line, EncodingJob transcodingJob)
+        private void ParseLogLine(string line, EncodingJobInfo state)
         {
             float? framerate = null;
             double? percent = null;
             TimeSpan? transcodingPosition = null;
             long? bytesTranscoded = null;
+            int? bitRate = null;
 
             var parts = line.Split(' ');
 
-            var totalMs = transcodingJob.RunTimeTicks.HasValue
-                ? TimeSpan.FromTicks(transcodingJob.RunTimeTicks.Value).TotalMilliseconds
+            var totalMs = state.RunTimeTicks.HasValue
+                ? TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalMilliseconds
                 : 0;
 
-            var startMs = transcodingJob.Options.StartTimeTicks.HasValue
-                ? TimeSpan.FromTicks(transcodingJob.Options.StartTimeTicks.Value).TotalMilliseconds
+            var startMs = state.BaseRequest.StartTimeTicks.HasValue
+                ? TimeSpan.FromTicks(state.BaseRequest.StartTimeTicks.Value).TotalMilliseconds
                 : 0;
 
             for (var i = 0; i < parts.Length; i++)
@@ -74,7 +80,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
                         framerate = val;
                     }
                 }
-                else if (transcodingJob.RunTimeTicks.HasValue &&
+                else if (state.RunTimeTicks.HasValue &&
                     part.StartsWith("time=", StringComparison.OrdinalIgnoreCase))
                 {
                     var time = part.Split(new[] { '=' }, 2).Last();
@@ -111,11 +117,32 @@ namespace MediaBrowser.MediaEncoding.Encoder
                         }
                     }
                 }
+                else if (part.StartsWith("bitrate=", StringComparison.OrdinalIgnoreCase))
+                {
+                    var rate = part.Split(new[] { '=' }, 2).Last();
+
+                    int? scale = null;
+                    if (rate.IndexOf("kbits/s", StringComparison.OrdinalIgnoreCase) != -1)
+                    {
+                        scale = 1024;
+                        rate = rate.Replace("kbits/s", string.Empty, StringComparison.OrdinalIgnoreCase);
+                    }
+
+                    if (scale.HasValue)
+                    {
+                        float val;
+
+                        if (float.TryParse(rate, NumberStyles.Any, _usCulture, out val))
+                        {
+                            bitRate = (int)Math.Ceiling(val * scale.Value);
+                        }
+                    }
+                }
             }
 
             if (framerate.HasValue || percent.HasValue)
             {
-                transcodingJob.ReportTranscodingProgress(transcodingPosition, framerate, percent, bytesTranscoded);
+                state.ReportTranscodingProgress(transcodingPosition, framerate, percent, bytesTranscoded, bitRate);
             }
         }
     }

+ 5 - 8
MediaBrowser.Controller/Net/StaticResultOptions.cs

@@ -23,21 +23,18 @@ namespace MediaBrowser.Controller.Net
         public Action OnComplete { get; set; }
         public Action OnError { get; set; }
 
+        public string Path { get; set; }
+
+        public FileShareMode FileShare { get; set; }
+
         public StaticResultOptions()
         {
             ResponseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+            FileShare = FileShareMode.Read;
         }
     }
 
     public class StaticFileResultOptions : StaticResultOptions
     {
-        public string Path { get; set; }
-
-        public FileShareMode FileShare { get; set; }
-
-        public StaticFileResultOptions()
-        {
-            FileShare = FileShareMode.Read;
-        }
     }
 }

+ 2 - 2
MediaBrowser.MediaEncoding/Encoder/AudioEncoder.cs

@@ -17,7 +17,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
         {
         }
 
-        protected override Task<string> GetCommandLineArguments(EncodingJob state)
+        protected override string GetCommandLineArguments(EncodingJob state)
         {
             var audioTranscodeParams = new List<string>();
 
@@ -78,7 +78,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
                 mapArgs,
                 metadata).Trim();
 
-            return Task.FromResult(result);
+            return result;
         }
 
         protected override string GetOutputFileExtension(EncodingJob state)

+ 4 - 4
MediaBrowser.MediaEncoding/Encoder/BaseEncoder.cs

@@ -66,7 +66,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
             IProgress<double> progress,
             CancellationToken cancellationToken)
         {
-            var encodingJob = await new EncodingJobFactory(Logger, LibraryManager, MediaSourceManager, ConfigurationManager)
+            var encodingJob = await new EncodingJobFactory(Logger, LibraryManager, MediaSourceManager, ConfigurationManager, MediaEncoder)
                 .CreateJob(options, EncodingHelper, IsVideoEncoder, progress, cancellationToken).ConfigureAwait(false);
 
             encodingJob.OutputFilePath = GetOutputFilePath(encodingJob);
@@ -76,7 +76,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
 
             await AcquireResources(encodingJob, cancellationToken).ConfigureAwait(false);
 
-            var commandLineArgs = await GetCommandLineArguments(encodingJob).ConfigureAwait(false);
+            var commandLineArgs = GetCommandLineArguments(encodingJob);
 
             var process = ProcessFactory.Create(new ProcessOptions
             {
@@ -242,7 +242,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
 
         private void OnTranscodeBeginning(EncodingJob job)
         {
-            job.ReportTranscodingProgress(null, null, null, null);
+            job.ReportTranscodingProgress(null, null, null, null, null);
         }
 
         private void OnTranscodeFailedToStart(string path, EncodingJob job)
@@ -265,7 +265,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
             return ConfigurationManager.GetConfiguration<EncodingOptions>("encoding");
         }
 
-        protected abstract Task<string> GetCommandLineArguments(EncodingJob job);
+        protected abstract string GetCommandLineArguments(EncodingJob job);
 
         private string GetOutputFilePath(EncodingJob state)
         {

+ 1 - 0
MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs

@@ -89,6 +89,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
             var found = new List<string>();
             var required = new[]
             {
+                "mpeg2video",
                 "h264_qsv",
                 "hevc_qsv",
                 "mpeg2_qsv",

+ 3 - 5
MediaBrowser.MediaEncoding/Encoder/EncodingJob.cs

@@ -36,7 +36,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
 
         public string MimeType { get; set; }
         public bool EstimateContentLength { get; set; }
-        public bool EnableMpegtsM2TsMode { get; set; }
         public TranscodeSeekInfo TranscodeSeekInfo { get; set; }
         public long? EncodingDurationTicks { get; set; }
         public string LiveStreamId { get; set; }
@@ -109,7 +108,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
         }
 
         public string OutputFilePath { get; set; }
-        public int? OutputAudioBitrate;
 
         public string ActualOutputVideoCodec
         {
@@ -379,7 +377,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
             return count;
         }
 
-        public void ReportTranscodingProgress(TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded)
+        public override void ReportTranscodingProgress(TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded, int? bitRate)
         {
             var ticks = transcodingPosition.HasValue ? transcodingPosition.Value.Ticks : (long?)null;
 
@@ -387,8 +385,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
 
             if (!percentComplete.HasValue && ticks.HasValue && RunTimeTicks.HasValue)
             {
-                var pct = ticks.Value/RunTimeTicks.Value;
-                percentComplete = pct*100;
+                var pct = ticks.Value / RunTimeTicks.Value;
+                percentComplete = pct * 100;
             }
 
             if (percentComplete.HasValue)

+ 10 - 1
MediaBrowser.MediaEncoding/Encoder/EncodingJobFactory.cs

@@ -22,15 +22,17 @@ namespace MediaBrowser.MediaEncoding.Encoder
         private readonly ILibraryManager _libraryManager;
         private readonly IMediaSourceManager _mediaSourceManager;
         private readonly IConfigurationManager _config;
+        private readonly IMediaEncoder _mediaEncoder;
 
         protected static readonly CultureInfo UsCulture = new CultureInfo("en-US");
         
-        public EncodingJobFactory(ILogger logger, ILibraryManager libraryManager, IMediaSourceManager mediaSourceManager, IConfigurationManager config)
+        public EncodingJobFactory(ILogger logger, ILibraryManager libraryManager, IMediaSourceManager mediaSourceManager, IConfigurationManager config, IMediaEncoder mediaEncoder)
         {
             _logger = logger;
             _libraryManager = libraryManager;
             _mediaSourceManager = mediaSourceManager;
             _config = config;
+            _mediaEncoder = mediaEncoder;
         }
 
         public async Task<EncodingJob> CreateJob(EncodingJobOptions options, EncodingHelper encodingHelper, bool isVideoRequest, IProgress<double> progress, CancellationToken cancellationToken)
@@ -61,6 +63,13 @@ namespace MediaBrowser.MediaEncoding.Encoder
                 request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault();
             }
 
+            if (!string.IsNullOrWhiteSpace(request.SubtitleCodec))
+            {
+                state.SupportedSubtitleCodecs = request.SubtitleCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToList();
+                request.SubtitleCodec = state.SupportedSubtitleCodecs.FirstOrDefault(i => _mediaEncoder.CanEncodeToSubtitleCodec(i))
+                    ?? state.SupportedSubtitleCodecs.FirstOrDefault();
+            }
+
             var item = _libraryManager.GetItemById(request.ItemId);
             state.ItemType = item.GetType().Name;
 

+ 3 - 159
MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs

@@ -647,9 +647,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
 
                     var videoStream = mediaInfo.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video);
 
-                    if (videoStream != null)
+                    if (videoStream != null && !videoStream.IsInterlaced)
                     {
-                        var isInterlaced = await DetectInterlaced(mediaInfo, videoStream, inputPath, probeSizeArgument).ConfigureAwait(false);
+                        var isInterlaced = DetectInterlaced(mediaInfo, videoStream);
 
                         if (isInterlaced)
                         {
@@ -672,7 +672,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
             }
         }
 
-        private async Task<bool> DetectInterlaced(MediaSourceInfo video, MediaStream videoStream, string inputPath, string probeSizeArgument)
+        private bool DetectInterlaced(MediaSourceInfo video, MediaStream videoStream)
         {
             var formats = (video.Container ?? string.Empty).Split(',').ToList();
             var enableInterlacedDection = formats.Contains("vob", StringComparer.OrdinalIgnoreCase) ||
@@ -698,165 +698,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
                 }
             }
 
-            if (video.Protocol != MediaProtocol.File)
-            {
-                return false;
-            }
-
-            var args = "{0} -i {1} -map 0:v:{2} -an -filter:v idet -frames:v 500 -an -f null /dev/null";
-
-            var process = _processFactory.Create(new ProcessOptions
-            {
-                CreateNoWindow = true,
-                UseShellExecute = false,
-
-                // Must consume both or ffmpeg may hang due to deadlocks. See comments below.   
-                RedirectStandardError = true,
-                FileName = FFMpegPath,
-                Arguments = string.Format(args, probeSizeArgument, inputPath, videoStream.Index.ToString(CultureInfo.InvariantCulture)).Trim(),
-
-                IsHidden = true,
-                ErrorDialog = false,
-                EnableRaisingEvents = true
-            });
-
-            _logger.Debug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
-            var idetFoundInterlaced = false;
-
-            using (var processWrapper = new ProcessWrapper(process, this, _logger))
-            {
-                try
-                {
-                    StartProcess(processWrapper);
-                }
-                catch (Exception ex)
-                {
-                    _logger.ErrorException("Error starting ffprobe", ex);
-
-                    throw;
-                }
-
-                try
-                {
-                    //process.BeginOutputReadLine();
-
-                    using (var reader = new StreamReader(process.StandardError.BaseStream))
-                    {
-                        while (!reader.EndOfStream)
-                        {
-                            var line = await reader.ReadLineAsync().ConfigureAwait(false);
-
-                            if (line.StartsWith("[Parsed_idet", StringComparison.OrdinalIgnoreCase))
-                            {
-                                var idetResult = AnalyzeIdetResult(line);
-
-                                if (idetResult.HasValue)
-                                {
-                                    if (!idetResult.Value)
-                                    {
-                                        return false;
-                                    }
-
-                                    idetFoundInterlaced = true;
-                                }
-                            }
-                        }
-                    }
-
-                }
-                catch
-                {
-                    StopProcess(processWrapper, 100);
-
-                    throw;
-                }
-            }
-
-            return idetFoundInterlaced;
-        }
-
-        private bool? AnalyzeIdetResult(string line)
-        {
-            // As you can see, the filter only guessed one frame as progressive. 
-            // Results like this are pretty typical. So if less than 30% of the detections are in the "Undetermined" category, then I only consider the video to be interlaced if at least 65% of the identified frames are in either the TFF or BFF category. 
-            // In this case (310 + 311)/(622) = 99.8% which is well over the 65% metric. I may refine that number with more testing but I honestly do not believe I will need to.
-            // http://awel.domblogger.net/videoTranscode/interlace.html
-            var index = line.IndexOf("detection:", StringComparison.OrdinalIgnoreCase);
-
-            if (index == -1)
-            {
-                return null;
-            }
-
-            line = line.Substring(index).Trim();
-            var parts = line.Split(' ').Where(i => !string.IsNullOrWhiteSpace(i)).Select(i => i.Trim()).ToList();
-
-            if (parts.Count < 2)
-            {
-                return null;
-            }
-            double tff = 0;
-            double bff = 0;
-            double progressive = 0;
-            double undetermined = 0;
-            double total = 0;
-
-            for (var i = 0; i < parts.Count - 1; i++)
-            {
-                var part = parts[i];
-
-                if (string.Equals(part, "tff:", StringComparison.OrdinalIgnoreCase))
-                {
-                    tff = GetNextPart(parts, i);
-                    total += tff;
-                }
-                else if (string.Equals(part, "bff:", StringComparison.OrdinalIgnoreCase))
-                {
-                    bff = GetNextPart(parts, i);
-                    total += tff;
-                }
-                else if (string.Equals(part, "progressive:", StringComparison.OrdinalIgnoreCase))
-                {
-                    progressive = GetNextPart(parts, i);
-                    total += progressive;
-                }
-                else if (string.Equals(part, "undetermined:", StringComparison.OrdinalIgnoreCase))
-                {
-                    undetermined = GetNextPart(parts, i);
-                    total += undetermined;
-                }
-            }
-
-            if (total == 0)
-            {
-                return null;
-            }
-
-            if ((undetermined / total) >= .3)
-            {
-                return false;
-            }
-
-            if (((tff + bff) / total) >= .4)
-            {
-                return true;
-            }
-
             return false;
         }
 
-        private int GetNextPart(List<string> parts, int index)
-        {
-            var next = parts[index + 1];
-
-            int value;
-            if (int.TryParse(next, NumberStyles.Any, CultureInfo.InvariantCulture, out value))
-            {
-                return value;
-            }
-            return 0;
-        }
-
         /// <summary>
         /// The us culture
         /// </summary>

+ 2 - 133
MediaBrowser.MediaEncoding/Encoder/VideoEncoder.cs

@@ -18,143 +18,12 @@ namespace MediaBrowser.MediaEncoding.Encoder
         {
         }
 
-        protected override async Task<string> GetCommandLineArguments(EncodingJob state)
+        protected override string GetCommandLineArguments(EncodingJob state)
         {
             // Get the output codec name
             var encodingOptions = GetEncodingOptions();
-            var videoCodec = EncodingHelper.GetVideoEncoder(state, encodingOptions);
 
-            var format = string.Empty;
-            var keyFrame = string.Empty;
-
-            if (string.Equals(Path.GetExtension(state.OutputFilePath), ".mp4", StringComparison.OrdinalIgnoreCase) &&
-                state.Options.Context == EncodingContext.Streaming)
-            {
-                // Comparison: https://github.com/jansmolders86/mediacenterjs/blob/master/lib/transcoding/desktop.js
-                format = " -f mp4 -movflags frag_keyframe+empty_moov";
-            }
-
-            var threads = EncodingHelper.GetNumberOfThreads(state, encodingOptions, string.Equals(videoCodec, "libvpx", StringComparison.OrdinalIgnoreCase));
-
-            var inputModifier = EncodingHelper.GetInputModifier(state, encodingOptions);
-
-            var videoArguments = await GetVideoArguments(state, videoCodec).ConfigureAwait(false);
-
-            return string.Format("{0} {1}{2} {3} {4} -map_metadata -1 -threads {5} {6}{7} -y \"{8}\"",
-                inputModifier,
-                EncodingHelper.GetInputArgument(state, encodingOptions),
-                keyFrame,
-                EncodingHelper.GetMapArgs(state),
-                videoArguments,
-                threads,
-                GetAudioArguments(state),
-                format,
-                state.OutputFilePath
-                ).Trim();
-        }
-
-        /// <summary>
-        /// Gets video arguments to pass to ffmpeg
-        /// </summary>
-        /// <param name="state">The state.</param>
-        /// <param name="videoCodec">The video codec.</param>
-        /// <returns>System.String.</returns>
-        private async Task<string> GetVideoArguments(EncodingJob state, string videoCodec)
-        {
-            var args = "-codec:v:0 " + videoCodec;
-
-            if (state.EnableMpegtsM2TsMode)
-            {
-                args += " -mpegts_m2ts_mode 1";
-            }
-
-            var isOutputMkv = string.Equals(state.Options.OutputContainer, "mkv", StringComparison.OrdinalIgnoreCase);
-
-            if (state.RunTimeTicks.HasValue)
-            {
-                //args += " -copyts -avoid_negative_ts disabled -start_at_zero";
-            }
-
-            if (string.Equals(videoCodec, "copy", StringComparison.OrdinalIgnoreCase))
-            {
-                if (state.VideoStream != null && EncodingHelper.IsH264(state.VideoStream) && string.Equals(state.Options.OutputContainer, "ts", StringComparison.OrdinalIgnoreCase) && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
-                {
-                    args += " -bsf:v h264_mp4toannexb";
-                }
-
-                return args;
-            }
-
-            var keyFrameArg = string.Format(" -force_key_frames \"expr:gte(t,n_forced*{0})\"",
-                5.ToString(UsCulture));
-
-            args += keyFrameArg;
-
-            var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.Options.SubtitleMethod == SubtitleDeliveryMethod.Encode;
-
-            // Add resolution params, if specified
-            if (!hasGraphicalSubs)
-            {
-                args += EncodingHelper.GetOutputSizeParam(state, videoCodec);
-            }
-
-            var qualityParam = EncodingHelper.GetVideoQualityParam(state, videoCodec, GetEncodingOptions(), "superfast");
-
-            if (!string.IsNullOrEmpty(qualityParam))
-            {
-                args += " " + qualityParam.Trim();
-            }
-
-            // This is for internal graphical subs
-            if (hasGraphicalSubs)
-            {
-                args += EncodingHelper.GetGraphicalSubtitleParam(state, videoCodec);
-            }
-
-            return args;
-        }
-
-        /// <summary>
-        /// Gets audio arguments to pass to ffmpeg
-        /// </summary>
-        /// <param name="state">The state.</param>
-        /// <returns>System.String.</returns>
-        private string GetAudioArguments(EncodingJob state)
-        {
-            // If the video doesn't have an audio stream, return a default.
-            if (state.AudioStream == null && state.VideoStream != null)
-            {
-                return string.Empty;
-            }
-
-            // Get the output codec name
-            var codec = EncodingHelper.GetAudioEncoder(state);
-
-            var args = "-codec:a:0 " + codec;
-
-            if (string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase))
-            {
-                return args;
-            }
-
-            // Add the number of audio channels
-            var channels = state.OutputAudioChannels;
-
-            if (channels.HasValue)
-            {
-                args += " -ac " + channels.Value;
-            }
-
-            var bitrate = state.OutputAudioBitrate;
-
-            if (bitrate.HasValue)
-            {
-                args += " -ab " + bitrate.Value.ToString(UsCulture);
-            }
-
-            args += " " + EncodingHelper.GetAudioFilterParam(state, GetEncodingOptions(), false);
-
-            return args;
+            return EncodingHelper.GetProgressiveVideoFullCommandLine(state, encodingOptions, state.OutputFilePath, "superfast");
         }
 
         protected override string GetOutputFileExtension(EncodingJob state)

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

@@ -55,7 +55,6 @@
     <Compile Include="Encoder\EncodingUtils.cs" />
     <Compile Include="Encoder\EncoderValidator.cs" />
     <Compile Include="Encoder\FontConfigLoader.cs" />
-    <Compile Include="Encoder\JobLogger.cs" />
     <Compile Include="Encoder\MediaEncoder.cs" />
     <Compile Include="Encoder\VideoEncoder.cs" />
     <Compile Include="Probing\FFProbeHelpers.cs" />

+ 2 - 0
MediaBrowser.MediaEncoding/Probing/InternalMediaInfoResult.cs

@@ -264,6 +264,8 @@ namespace MediaBrowser.MediaEncoding.Probing
         /// <value>The loro_surmixlev.</value>
         public string loro_surmixlev { get; set; }
 
+        public string field_order { get; set; }
+
         /// <summary>
         /// Gets or sets the disposition.
         /// </summary>

+ 5 - 0
MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs

@@ -508,6 +508,11 @@ namespace MediaBrowser.MediaEncoding.Probing
                 stream.IsAVC = false;
             }
 
+            if (!string.IsNullOrWhiteSpace(streamInfo.field_order) && !string.Equals(streamInfo.field_order, "progressive", StringComparison.OrdinalIgnoreCase))
+            {
+                stream.IsInterlaced = true;
+            }
+
             // Filter out junk
             if (!string.IsNullOrWhiteSpace(streamInfo.codec_tag_string) && streamInfo.codec_tag_string.IndexOf("[0]", StringComparison.OrdinalIgnoreCase) == -1)
             {

+ 7 - 12
MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs

@@ -734,16 +734,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles
                 }
             }
 
-            var charsetFromLanguage = string.IsNullOrWhiteSpace(language)
-                ? null
-                : GetSubtitleFileCharacterSetFromLanguage(language);
-
-            // This assumption should only be made for external subtitles
-            if (!string.IsNullOrWhiteSpace(charsetFromLanguage) && !string.Equals(charsetFromLanguage, "windows-1252", StringComparison.OrdinalIgnoreCase))
-            {
-                return charsetFromLanguage;
-            }
-
             var charset = await DetectCharset(path, language, protocol, cancellationToken).ConfigureAwait(false);
 
             if (!string.IsNullOrWhiteSpace(charset))
@@ -756,7 +746,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles
                 return charset;
             }
 
-            return charsetFromLanguage;
+            if (!string.IsNullOrWhiteSpace(language))
+            {
+                return GetSubtitleFileCharacterSetFromLanguage(language);
+            }
+
+            return null;
         }
 
         public string GetSubtitleFileCharacterSetFromLanguage(string language)
@@ -854,4 +849,4 @@ namespace MediaBrowser.MediaEncoding.Subtitles
             throw new ArgumentOutOfRangeException("protocol");
         }
     }
-}
+}

+ 2 - 0
MediaBrowser.Model/Configuration/LibraryOptions.cs

@@ -16,6 +16,8 @@
         public bool EnableAutomaticSeriesGrouping { get; set; }
         public bool EnableEmbeddedTitles { get; set; }
 
+        public int AutomaticRefreshIntervalDays { get; set; }
+
         /// <summary>
         /// Gets or sets the preferred metadata language.
         /// </summary>

+ 1 - 0
MediaBrowser.Model/Configuration/ServerConfiguration.cs

@@ -48,6 +48,7 @@ namespace MediaBrowser.Model.Configuration
         public bool EnableHttps { get; set; }
         public bool EnableSeriesPresentationUniqueKey { get; set; }
         public bool EnableLocalizedGuids { get; set; }
+        public bool EnableNormalizedItemByNameIds { get; set; }
 
         /// <summary>
         /// Gets or sets the value pointing to the file system where the ssl certiifcate is located..

+ 2 - 1
MediaBrowser.Model/Dlna/ProfileConditionValue.cs

@@ -21,6 +21,7 @@
         NumVideoStreams = 17,
         IsSecondaryAudio = 18,
         VideoCodecTag = 19,
-        IsAvc = 20
+        IsAvc = 20,
+        IsInterlaced = 21
     }
 }

+ 55 - 8
MediaBrowser.Model/Dlna/StreamBuilder.cs

@@ -231,6 +231,7 @@ namespace MediaBrowser.Model.Dlna
                 {
                     playlistItem.AudioCodecs = transcodingProfile.AudioCodec.Split(',');
                 }
+
                 playlistItem.SubProtocol = transcodingProfile.Protocol;
 
                 List<CodecProfile> audioCodecProfiles = new List<CodecProfile>();
@@ -323,7 +324,7 @@ namespace MediaBrowser.Model.Dlna
             if (directPlayProfile != null)
             {
                 // While options takes the network and other factors into account. Only applies to direct stream
-                if (item.SupportsDirectStream && IsAudioEligibleForDirectPlay(item, options.GetMaxBitrate(true)) && options.EnableDirectStream)
+                if (item.SupportsDirectStream && IsAudioEligibleForDirectPlay(item, options.GetMaxBitrate(true), PlayMethod.DirectStream) && options.EnableDirectStream)
                 {
                     playMethods.Add(PlayMethod.DirectStream);
                 }
@@ -331,7 +332,7 @@ namespace MediaBrowser.Model.Dlna
                 // The profile describes what the device supports
                 // If device requirements are satisfied then allow both direct stream and direct play
                 if (item.SupportsDirectPlay &&
-                    IsAudioEligibleForDirectPlay(item, GetBitrateForDirectPlayCheck(item, options, true)) && options.EnableDirectPlay)
+                    IsAudioEligibleForDirectPlay(item, GetBitrateForDirectPlayCheck(item, options, true), PlayMethod.DirectPlay) && options.EnableDirectPlay)
                 {
                     playMethods.Add(PlayMethod.DirectPlay);
                 }
@@ -479,10 +480,19 @@ namespace MediaBrowser.Model.Dlna
 
                 playlistItem.AudioCodecs = transcodingProfile.AudioCodec.Split(',');
 
-                playlistItem.VideoCodec = transcodingProfile.VideoCodec;
+                playlistItem.VideoCodecs = transcodingProfile.VideoCodec.Split(',');
                 playlistItem.CopyTimestamps = transcodingProfile.CopyTimestamps;
                 playlistItem.EnableSubtitlesInManifest = transcodingProfile.EnableSubtitlesInManifest;
 
+                if (transcodingProfile.MinSegments > 0)
+                {
+                    playlistItem.MinSegments = transcodingProfile.MinSegments;
+                }
+                if (transcodingProfile.SegmentLength > 0)
+                {
+                    playlistItem.SegmentLength = transcodingProfile.SegmentLength;
+                }
+
                 if (!string.IsNullOrEmpty(transcodingProfile.MaxAudioChannels))
                 {
                     int transcodingMaxAudioChannels;
@@ -895,7 +905,7 @@ namespace MediaBrowser.Model.Dlna
                 }
             }
 
-            return IsAudioEligibleForDirectPlay(item, maxBitrate);
+            return IsAudioEligibleForDirectPlay(item, maxBitrate, playMethod);
         }
 
         public static SubtitleProfile GetSubtitleProfile(MediaStream subtitleStream, SubtitleProfile[] subtitleProfiles, PlayMethod playMethod, string transcodingSubProtocol, string transcodingContainer)
@@ -1025,23 +1035,29 @@ namespace MediaBrowser.Model.Dlna
             return null;
         }
 
-        private bool IsAudioEligibleForDirectPlay(MediaSourceInfo item, long? maxBitrate)
+        private bool IsAudioEligibleForDirectPlay(MediaSourceInfo item, long? maxBitrate, PlayMethod playMethod)
         {
+            // Don't restrict by bitrate if coming from an external domain
+            if (item.IsRemote)
+            {
+                return true;    
+            }
+
             if (!maxBitrate.HasValue)
             {
-                _logger.Info("Cannot direct play due to unknown supported bitrate");
+                _logger.Info("Cannot "+ playMethod + " due to unknown supported bitrate");
                 return false;
             }
 
             if (!item.Bitrate.HasValue)
             {
-                _logger.Info("Cannot direct play due to unknown content bitrate");
+                _logger.Info("Cannot " + playMethod + " due to unknown content bitrate");
                 return false;
             }
 
             if (item.Bitrate.Value > maxBitrate.Value)
             {
-                _logger.Info("Bitrate exceeds DirectPlay limit: media bitrate: {0}, max bitrate: {1}", item.Bitrate.Value.ToString(CultureInfo.InvariantCulture), maxBitrate.Value.ToString(CultureInfo.InvariantCulture));
+                _logger.Info("Bitrate exceeds " + playMethod + " limit: media bitrate: {0}, max bitrate: {1}", item.Bitrate.Value.ToString(CultureInfo.InvariantCulture), maxBitrate.Value.ToString(CultureInfo.InvariantCulture));
                 return false;
             }
 
@@ -1137,6 +1153,37 @@ namespace MediaBrowser.Model.Dlna
                             break;
                         }
                     case ProfileConditionValue.IsAnamorphic:
+                        {
+                            bool isAnamorphic;
+                            if (bool.TryParse(value, out isAnamorphic))
+                            {
+                                if (isAnamorphic && condition.Condition == ProfileConditionType.Equals)
+                                {
+                                    item.RequireNonAnamorphic = true;
+                                }
+                                else if (!isAnamorphic && condition.Condition == ProfileConditionType.NotEquals)
+                                {
+                                    item.RequireNonAnamorphic = true;
+                                }
+                            }
+                            break;
+                        }
+                    case ProfileConditionValue.IsInterlaced:
+                        {
+                            bool isInterlaced;
+                            if (bool.TryParse(value, out isInterlaced))
+                            {
+                                if (isInterlaced && condition.Condition == ProfileConditionType.Equals)
+                                {
+                                    item.DeInterlace = true;
+                                }
+                                else if (!isInterlaced && condition.Condition == ProfileConditionType.NotEquals)
+                                {
+                                    item.DeInterlace = true;
+                                }
+                            }
+                            break;
+                        }
                     case ProfileConditionValue.AudioProfile:
                     case ProfileConditionValue.Has64BitOffsets:
                     case ProfileConditionValue.PacketLength:

+ 61 - 5
MediaBrowser.Model/Dlna/StreamInfo.cs

@@ -6,6 +6,7 @@ using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.Session;
 using System;
 using System.Collections.Generic;
+using System.Globalization;
 using System.Linq;
 
 namespace MediaBrowser.Model.Dlna
@@ -18,6 +19,7 @@ namespace MediaBrowser.Model.Dlna
         public StreamInfo()
         {
             AudioCodecs = new string[] { };
+            VideoCodecs = new string[] { };
             SubtitleCodecs = new string[] { };
         }
 
@@ -34,13 +36,18 @@ namespace MediaBrowser.Model.Dlna
 
         public long StartPositionTicks { get; set; }
 
-        public string VideoCodec { get; set; }
         public string VideoProfile { get; set; }
 
+        public int? SegmentLength { get; set; }
+        public int? MinSegments { get; set; }
+
         public bool RequireAvc { get; set; }
+        public bool DeInterlace { get; set; }
+        public bool RequireNonAnamorphic { get; set; }
         public bool CopyTimestamps { get; set; }
         public bool EnableSubtitlesInManifest { get; set; }
         public string[] AudioCodecs { get; set; }
+        public string[] VideoCodecs { get; set; }
 
         public int? AudioStreamIndex { get; set; }
 
@@ -204,11 +211,15 @@ namespace MediaBrowser.Model.Dlna
                 string.Empty :
                 string.Join(",", item.AudioCodecs);
 
+            string videoCodecs = item.VideoCodecs.Length == 0 ?
+                string.Empty :
+                string.Join(",", item.VideoCodecs);
+
             list.Add(new NameValuePair("DeviceProfileId", item.DeviceProfileId ?? string.Empty));
             list.Add(new NameValuePair("DeviceId", item.DeviceId ?? string.Empty));
             list.Add(new NameValuePair("MediaSourceId", item.MediaSourceId ?? string.Empty));
             list.Add(new NameValuePair("Static", item.IsDirectStream.ToString().ToLower()));
-            list.Add(new NameValuePair("VideoCodec", item.VideoCodec ?? string.Empty));
+            list.Add(new NameValuePair("VideoCodec", videoCodecs));
             list.Add(new NameValuePair("AudioCodec", audioCodecs));
             list.Add(new NameValuePair("AudioStreamIndex", item.AudioStreamIndex.HasValue ? StringHelper.ToStringCultureInvariant(item.AudioStreamIndex.Value) : string.Empty));
             list.Add(new NameValuePair("SubtitleStreamIndex", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? StringHelper.ToStringCultureInvariant(item.SubtitleStreamIndex.Value) : string.Empty));
@@ -232,7 +243,9 @@ namespace MediaBrowser.Model.Dlna
             //    }
             //}
 
-            if (StringHelper.EqualsIgnoreCase(item.SubProtocol, "hls") && !forceStartPosition)
+            var isHls = StringHelper.EqualsIgnoreCase(item.SubProtocol, "hls");
+
+            if (isHls && !forceStartPosition)
             {
                 list.Add(new NameValuePair("StartTimeTicks", string.Empty));
             }
@@ -276,6 +289,24 @@ namespace MediaBrowser.Model.Dlna
 
             list.Add(new NameValuePair("SubtitleCodec", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Embed ? subtitleCodecs : string.Empty));
 
+            list.Add(new NameValuePair("RequireNonAnamorphic", item.RequireNonAnamorphic.ToString().ToLower()));
+            list.Add(new NameValuePair("DeInterlace", item.DeInterlace.ToString().ToLower()));
+
+            if (!isDlna && isHls)
+            {
+                list.Add(new NameValuePair("SegmentContainer", item.Container ?? string.Empty));
+
+                if (item.SegmentLength.HasValue)
+                {
+                    list.Add(new NameValuePair("SegmentLength", item.SegmentLength.Value.ToString(CultureInfo.InvariantCulture)));
+                }
+
+                if (item.MinSegments.HasValue)
+                {
+                    list.Add(new NameValuePair("MinSegments", item.MinSegments.Value.ToString(CultureInfo.InvariantCulture)));
+                }
+            }
+
             return list;
         }
 
@@ -609,9 +640,34 @@ namespace MediaBrowser.Model.Dlna
             }
         }
 
+        public string TargetVideoCodec
+        {
+            get
+            {
+                MediaStream stream = TargetVideoStream;
+
+                string inputCodec = stream == null ? null : stream.Codec;
+
+                if (IsDirectStream)
+                {
+                    return inputCodec;
+                }
+
+                foreach (string codec in VideoCodecs)
+                {
+                    if (StringHelper.EqualsIgnoreCase(codec, inputCodec))
+                    {
+                        return codec;
+                    }
+                }
+
+                return VideoCodecs.Length == 0 ? null : VideoCodecs[0];
+            }
+        }
+        
         /// <summary>
-        /// Predicts the audio channels that will be in the output stream
-        /// </summary>
+             /// Predicts the audio channels that will be in the output stream
+             /// </summary>
         public long? TargetSize
         {
             get

+ 6 - 0
MediaBrowser.Model/Dlna/TranscodingProfile.cs

@@ -42,6 +42,12 @@ namespace MediaBrowser.Model.Dlna
         [XmlAttribute("maxAudioChannels")]
         public string MaxAudioChannels { get; set; }
 
+        [XmlAttribute("minSegments")]
+        public int MinSegments { get; set; }
+
+        [XmlAttribute("segmentLength")]
+        public int SegmentLength { get; set; }
+
         public List<string> GetAudioCodecs()
         {
             List<string> list = new List<string>();

+ 2 - 0
MediaBrowser.Model/IO/IFileSystem.cs

@@ -10,6 +10,8 @@ namespace MediaBrowser.Model.IO
     /// </summary>
     public interface IFileSystem
     {
+        void AddShortcutHandler(IShortcutHandler handler);
+
         /// <summary>
         /// Determines whether the specified filename is shortcut.
         /// </summary>

+ 2 - 3
MediaBrowser.Model/LiveTv/LiveTvOptions.cs

@@ -34,7 +34,7 @@ namespace MediaBrowser.Model.LiveTv
             TunerHosts = new List<TunerHostInfo>();
             ListingProviders = new List<ListingsProviderInfo>();
             MediaLocationsCreated = new string[] { };
-            RecordingEncodingFormat = "mp4";
+            RecordingEncodingFormat = "mkv";
             RecordingPostProcessorArguments = "\"{path}\"";
             EnableRecordingEncoding = true;
         }
@@ -46,14 +46,13 @@ namespace MediaBrowser.Model.LiveTv
         public string Url { get; set; }
         public string Type { get; set; }
         public string DeviceId { get; set; }
+        public string FriendlyName { get; set; }
         public bool ImportFavoritesOnly { get; set; }
         public bool AllowHWTranscoding { get; set; }
-        public bool IsEnabled { get; set; }
         public bool EnableTvgId { get; set; }
 
         public TunerHostInfo()
         {
-            IsEnabled = true;
             AllowHWTranscoding = true;
         }
     }

+ 2 - 0
MediaBrowser.Model/MediaInfo/PlaybackInfoRequest.cs

@@ -27,9 +27,11 @@ namespace MediaBrowser.Model.MediaInfo
         public bool EnableDirectPlay { get; set; }
         public bool EnableDirectStream { get; set; }
         public bool EnableTranscoding { get; set; }
+        public bool ForceDirectPlayRemoteMediaSource { get; set; }
 
         public PlaybackInfoRequest()
         {
+            ForceDirectPlayRemoteMediaSource = true;
             EnableDirectPlay = true;
             EnableDirectStream = true;
             EnableTranscoding = true;

+ 3 - 0
MediaBrowser.Model/Net/IAcceptSocket.cs

@@ -1,4 +1,6 @@
 using System;
+using System.Threading;
+using System.Threading.Tasks;
 
 namespace MediaBrowser.Model.Net
 {
@@ -13,6 +15,7 @@ namespace MediaBrowser.Model.Net
         void Bind(IpEndPointInfo endpoint);
         void Connect(IpEndPointInfo endPoint);
         void StartAccept(Action<IAcceptSocket> onAccept, Func<bool> isClosed);
+        Task SendFile(string path, byte[] preBuffer, byte[] postBuffer, CancellationToken cancellationToken);
     }
 
     public class SocketCreateException : Exception

+ 1 - 0
MediaBrowser.Model/Net/ISocket.cs

@@ -24,5 +24,6 @@ namespace MediaBrowser.Model.Net
         /// Sends a UDP message to a particular end point (uni or multicast).
         /// </summary>
         Task SendAsync(byte[] buffer, int bytes, IpEndPointInfo endPoint, CancellationToken cancellationToken);
+        Task SendWithLockAsync(byte[] buffer, int bytes, IpEndPointInfo endPoint, CancellationToken cancellationToken);
     }
 }

+ 2 - 0
MediaBrowser.Model/Net/ISocketFactory.cs

@@ -14,6 +14,8 @@ namespace MediaBrowser.Model.Net
 		/// <returns>A <see cref="ISocket"/> implementation.</returns>
 		ISocket CreateUdpSocket(int localPort);
 
+        ISocket CreateUdpBroadcastSocket(int localPort);
+
         ISocket CreateTcpSocket(IpAddressInfo remoteAddress, int remotePort);
 
         /// <summary>

+ 3 - 0
MediaBrowser.Model/Querying/NextUpQuery.cs

@@ -55,9 +55,12 @@ namespace MediaBrowser.Model.Querying
         /// <value>The enable image types.</value>
         public ImageType[] EnableImageTypes { get; set; }
 
+        public bool EnableTotalRecordCount { get; set; }
+
         public NextUpQuery()
         {
             EnableImageTypes = new ImageType[] {};
+            EnableTotalRecordCount = true;
         }
     }
 }

Some files were not shown because too many files changed in this diff