Pārlūkot izejas kodu

Added the ability for the server to handle byte-range requests, and also added a static file handler to utilize it

LukePulverenti Luke Pulverenti luke pulverenti 13 gadi atpakaļ
vecāks
revīzija
2536011247

+ 7 - 10
MediaBrowser.Api/HttpHandlers/ImageHandler.cs

@@ -46,18 +46,15 @@ namespace MediaBrowser.Api.HttpHandlers
             }
         }
 
-        public override DateTime? LastDateModified
+        protected override DateTime? GetLastDateModified()
         {
-            get
+            try
             {
-                try
-                {
-                    return File.GetLastWriteTime(ImagePath);
-                }
-                catch
-                {
-                    return null;
-                }
+                return File.GetLastWriteTime(ImagePath);
+            }
+            catch
+            {
+                return base.GetLastDateModified();
             }
         }
 

+ 20 - 23
MediaBrowser.Api/Plugin.cs

@@ -1,8 +1,8 @@
 using System;
 using System.ComponentModel.Composition;
+using System.Net;
 using System.Reactive.Linq;
 using MediaBrowser.Api.HttpHandlers;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Net.Handlers;
 using MediaBrowser.Common.Plugins;
 using MediaBrowser.Controller;
@@ -22,78 +22,75 @@ namespace MediaBrowser.Api
         {
             var httpServer = Kernel.Instance.HttpServer;
 
-            httpServer.Where(ctx => ctx.LocalPath.IndexOf("/api/", StringComparison.OrdinalIgnoreCase) != -1).Subscribe(ctx =>
+            httpServer.Where(ctx => ctx.Request.Url.LocalPath.IndexOf("/api/", StringComparison.OrdinalIgnoreCase) != -1).Subscribe(ctx =>
             {
                 BaseHandler handler = GetHandler(ctx);
 
                 if (handler != null)
                 {
-                    ctx.Respond(handler);
+                    handler.ProcessRequest(ctx);
                 }
             });
         }
 
-        private BaseHandler GetHandler(RequestContext ctx)
+        private BaseHandler GetHandler(HttpListenerContext ctx)
         {
-            BaseHandler handler = null;
-
-            string localPath = ctx.LocalPath;
+            string localPath = ctx.Request.Url.LocalPath;
 
             if (localPath.EndsWith("/api/item", StringComparison.OrdinalIgnoreCase))
             {
-                handler = new ItemHandler();
+                return new ItemHandler();
             }
             else if (localPath.EndsWith("/api/image", StringComparison.OrdinalIgnoreCase))
             {
-                handler = new ImageHandler();
+                return new ImageHandler();
             } 
             else if (localPath.EndsWith("/api/users", StringComparison.OrdinalIgnoreCase))
             {
-                handler = new UsersHandler();
+                return new UsersHandler();
             }
             else if (localPath.EndsWith("/api/genre", StringComparison.OrdinalIgnoreCase))
             {
-                handler = new GenreHandler();
+                return new GenreHandler();
             }
             else if (localPath.EndsWith("/api/genres", StringComparison.OrdinalIgnoreCase))
             {
-                handler = new GenresHandler();
+                return new GenresHandler();
             }
             else if (localPath.EndsWith("/api/studio", StringComparison.OrdinalIgnoreCase))
             {
-                handler = new StudioHandler();
+                return new StudioHandler();
             }
             else if (localPath.EndsWith("/api/studios", StringComparison.OrdinalIgnoreCase))
             {
-                handler = new StudiosHandler();
+                return new StudiosHandler();
             }
             else if (localPath.EndsWith("/api/recentlyaddeditems", StringComparison.OrdinalIgnoreCase))
             {
-                handler = new RecentlyAddedItemsHandler();
+                return new RecentlyAddedItemsHandler();
             }
             else if (localPath.EndsWith("/api/inprogressitems", StringComparison.OrdinalIgnoreCase))
             {
-                handler = new InProgressItemsHandler();
+                return new InProgressItemsHandler();
             }
             else if (localPath.EndsWith("/api/userconfiguration", StringComparison.OrdinalIgnoreCase))
             {
-                handler = new UserConfigurationHandler();
+                return new UserConfigurationHandler();
             }
             else if (localPath.EndsWith("/api/plugins", StringComparison.OrdinalIgnoreCase))
             {
-                handler = new PluginsHandler();
+                return new PluginsHandler();
             }
             else if (localPath.EndsWith("/api/pluginconfiguration", StringComparison.OrdinalIgnoreCase))
             {
-                handler = new PluginConfigurationHandler();
+                return new PluginConfigurationHandler();
             }
-
-            if (handler != null)
+            else if (localPath.EndsWith("/api/static", StringComparison.OrdinalIgnoreCase))
             {
-                handler.RequestContext = ctx;
+                return new StaticFileHandler();
             }
 
-            return handler;
+            return null;
         }
     }
 }

+ 1 - 1
MediaBrowser.Common/MediaBrowser.Common.csproj

@@ -58,6 +58,7 @@
     <Compile Include="Configuration\ApplicationPaths.cs" />
     <Compile Include="Configuration\BaseApplicationConfiguration.cs" />
     <Compile Include="Events\GenericItemEventArgs.cs" />
+    <Compile Include="Net\Handlers\StaticFileHandler.cs" />
     <Compile Include="Net\MimeTypes.cs" />
     <Compile Include="Serialization\JsonSerializer.cs" />
     <Compile Include="Kernel\BaseKernel.cs" />
@@ -73,7 +74,6 @@
     <Compile Include="Net\Handlers\BaseJsonHandler.cs" />
     <Compile Include="Net\HttpServer.cs" />
     <Compile Include="Net\Request.cs" />
-    <Compile Include="Net\RequestContext.cs" />
     <Compile Include="Net\StreamExtensions.cs" />
     <Compile Include="Plugins\BasePlugin.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />

+ 220 - 67
MediaBrowser.Common/Net/Handlers/BaseHandler.cs

@@ -3,32 +3,36 @@ using System.Collections.Generic;
 using System.Collections.Specialized;
 using System.IO;
 using System.IO.Compression;
+using System.Linq;
 using System.Net;
+using MediaBrowser.Common.Logging;
 
 namespace MediaBrowser.Common.Net.Handlers
 {
     public abstract class BaseHandler
     {
-        /// <summary>
-        /// Response headers
-        /// </summary>
-        public IDictionary<string, string> Headers = new Dictionary<string, string>();
-
         private Stream CompressedStream { get; set; }
 
-        public virtual bool UseChunkedEncoding
+        public virtual bool? UseChunkedEncoding
         {
             get
             {
-                return true;
+                return null;
             }
         }
 
-        public virtual long? ContentLength
+        private bool _TotalContentLengthDiscovered = false;
+        private long? _TotalContentLength = null;
+        public long? TotalContentLength
         {
             get
             {
-                return null;
+                if (!_TotalContentLengthDiscovered)
+                {
+                    _TotalContentLength = GetTotalContentLength();
+                }
+
+                return _TotalContentLength;
             }
         }
 
@@ -44,29 +48,18 @@ namespace MediaBrowser.Common.Net.Handlers
             }
         }
 
-        /// <summary>
-        /// The action to write the response to the output stream
-        /// </summary>
-        public Action<Stream> WriteStream
+        protected virtual bool SupportsByteRangeRequests
         {
             get
             {
-                return s =>
-                {
-                    WriteReponse(s);
-
-                    if (!IsAsyncHandler)
-                    {
-                        DisposeResponseStream();
-                    }
-                };
+                return false;
             }
         }
 
         /// <summary>
-        /// The original RequestContext
+        /// The original HttpListenerContext
         /// </summary>
-        public RequestContext RequestContext { get; set; }
+        protected HttpListenerContext HttpListenerContext { get; private set; }
 
         /// <summary>
         /// The original QueryString
@@ -75,7 +68,54 @@ namespace MediaBrowser.Common.Net.Handlers
         {
             get
             {
-                return RequestContext.Request.QueryString;
+                return HttpListenerContext.Request.QueryString;
+            }
+        }
+
+        protected List<KeyValuePair<long, long?>> _RequestedRanges = null;
+        protected IEnumerable<KeyValuePair<long, long?>> RequestedRanges
+        {
+            get
+            {
+                if (_RequestedRanges == null)
+                {
+                    _RequestedRanges = new List<KeyValuePair<long, long?>>();
+
+                    if (IsRangeRequest)
+                    {
+                        // Example: bytes=0-,32-63
+                        string[] ranges = HttpListenerContext.Request.Headers["Range"].Split('=')[1].Split(',');
+
+                        foreach (string range in ranges)
+                        {
+                            string[] vals = range.Split('-');
+
+                            long start = 0;
+                            long? end = null;
+
+                            if (!string.IsNullOrEmpty(vals[0]))
+                            {
+                                start = long.Parse(vals[0]);
+                            }
+                            if (!string.IsNullOrEmpty(vals[1]))
+                            {
+                                end = long.Parse(vals[1]);
+                            }
+
+                            _RequestedRanges.Add(new KeyValuePair<long, long?>(start, end));
+                        }
+                    }
+                }
+
+                return _RequestedRanges;
+            }
+        }
+
+        protected bool IsRangeRequest
+        {
+            get
+            {
+                return HttpListenerContext.Request.Headers.AllKeys.Contains("Range");
             }
         }
 
@@ -87,13 +127,7 @@ namespace MediaBrowser.Common.Net.Handlers
         /// <summary>
         /// Gets the status code to include in the response headers
         /// </summary>
-        public virtual int StatusCode
-        {
-            get
-            {
-                return 200;
-            }
-        }
+        protected int StatusCode { get; set; }
 
         /// <summary>
         /// Gets the cache duration to include in the response headers
@@ -106,18 +140,25 @@ namespace MediaBrowser.Common.Net.Handlers
             }
         }
 
+        private bool _LastDateModifiedDiscovered = false;
+        private DateTime? _LastDateModified = null;
         /// <summary>
         /// Gets the last date modified of the content being returned, if this can be determined.
         /// This will be used to invalidate the cache, so it's not needed if CacheDuration is 0.
         /// </summary>
-        public virtual DateTime? LastDateModified
+        public DateTime? LastDateModified
         {
             get
             {
-                return null;
+                if (!_LastDateModifiedDiscovered)
+                {
+                    _LastDateModified = GetLastDateModified();
+                }
+
+                return _LastDateModified;
             }
         }
-
+        
         public virtual bool CompressResponse
         {
             get
@@ -130,7 +171,7 @@ namespace MediaBrowser.Common.Net.Handlers
         {
             get
             {
-                string enc = RequestContext.Request.Headers["Accept-Encoding"] ?? string.Empty;
+                string enc = HttpListenerContext.Request.Headers["Accept-Encoding"] ?? string.Empty;
 
                 return enc.IndexOf("deflate", StringComparison.OrdinalIgnoreCase) != -1 || enc.IndexOf("gzip", StringComparison.OrdinalIgnoreCase) != -1;
             }
@@ -140,7 +181,7 @@ namespace MediaBrowser.Common.Net.Handlers
         {
             get
             {
-                string enc = RequestContext.Request.Headers["Accept-Encoding"] ?? string.Empty;
+                string enc = HttpListenerContext.Request.Headers["Accept-Encoding"] ?? string.Empty;
 
                 if (enc.IndexOf("deflate", StringComparison.OrdinalIgnoreCase) != -1)
                 {
@@ -155,64 +196,138 @@ namespace MediaBrowser.Common.Net.Handlers
             }
         }
 
-        protected virtual void PrepareResponseBeforeWriteOutput(HttpListenerResponse response)
+        public void ProcessRequest(HttpListenerContext ctx)
         {
-            // Don't force this to true. HttpListener will default it to true if supported by the client.
-            if (!UseChunkedEncoding)
+            HttpListenerContext = ctx;
+
+            Logger.LogInfo("Http Server received request at: " + ctx.Request.Url.ToString());
+            Logger.LogInfo("Http Headers: " + string.Join(",", ctx.Request.Headers.AllKeys.Select(k => k + "=" + ctx.Request.Headers[k])));
+
+            ctx.Response.AddHeader("Access-Control-Allow-Origin", "*");
+
+            ctx.Response.KeepAlive = true;
+
+            if (SupportsByteRangeRequests && IsRangeRequest)
             {
-                response.SendChunked = false;
+                ctx.Response.Headers["Accept-Ranges"] = "bytes";
             }
+            
+            // Set the initial status code
+            // When serving a range request, we need to return status code 206 to indicate a partial response body
+            StatusCode = SupportsByteRangeRequests && IsRangeRequest ? 206 : 200;
+
+            ctx.Response.ContentType = ContentType;
 
-            if (ContentLength.HasValue)
+            TimeSpan cacheDuration = CacheDuration;
+
+            if (ctx.Request.Headers.AllKeys.Contains("If-Modified-Since"))
             {
-                response.ContentLength64 = ContentLength.Value;
+                DateTime ifModifiedSince;
+
+                if (DateTime.TryParse(ctx.Request.Headers["If-Modified-Since"].Replace(" GMT", string.Empty), out ifModifiedSince))
+                {
+                    // If the cache hasn't expired yet just return a 304
+                    if (IsCacheValid(ifModifiedSince, cacheDuration, LastDateModified))
+                    {
+                        StatusCode = 304;
+                    }
+                }
             }
 
-            if (CompressResponse && ClientSupportsCompression)
+            if (StatusCode == 200 || StatusCode == 206)
             {
-                response.AddHeader("Content-Encoding", CompressionMethod);
+                ProcessUncachedResponse(ctx, cacheDuration);
             }
-
-            TimeSpan cacheDuration = CacheDuration;
-            
-            if (cacheDuration.Ticks > 0)
+            else
             {
-                CacheResponse(response, cacheDuration, LastDateModified);
+                ctx.Response.StatusCode = StatusCode;
+                ctx.Response.SendChunked = false;
+                DisposeResponseStream();
             }
         }
 
-        private void CacheResponse(HttpListenerResponse response, TimeSpan duration, DateTime? dateModified)
+        private void ProcessUncachedResponse(HttpListenerContext ctx, TimeSpan cacheDuration)
         {
-            DateTime lastModified = dateModified ?? DateTime.Now;
+            long? totalContentLength = TotalContentLength;
 
-            response.Headers[HttpResponseHeader.CacheControl] = "public, max-age=" + Convert.ToInt32(duration.TotalSeconds);
-            response.Headers[HttpResponseHeader.Expires] = DateTime.Now.Add(duration).ToString("r");
-            response.Headers[HttpResponseHeader.LastModified] = lastModified.ToString("r");
-        }
+            // By default, use chunked encoding if we don't know the content length
+            bool useChunkedEncoding = UseChunkedEncoding == null ? (totalContentLength == null) : UseChunkedEncoding.Value;
 
-        private void WriteReponse(Stream stream)
-        {
-            PrepareResponseBeforeWriteOutput(RequestContext.Response);
+            // Don't force this to true. HttpListener will default it to true if supported by the client.
+            if (!useChunkedEncoding)
+            {
+                ctx.Response.SendChunked = false;
+            }
+
+            // Set the content length, if we know it
+            if (totalContentLength.HasValue)
+            {
+                ctx.Response.ContentLength64 = totalContentLength.Value;
+            }
 
+            // Add the compression header
             if (CompressResponse && ClientSupportsCompression)
             {
-                if (CompressionMethod.Equals("deflate", StringComparison.OrdinalIgnoreCase))
+                ctx.Response.AddHeader("Content-Encoding", CompressionMethod);
+            }
+
+            // Add caching headers
+            if (cacheDuration.Ticks > 0)
+            {
+                CacheResponse(ctx.Response, cacheDuration, LastDateModified);
+            }
+
+            PrepareUncachedResponse(ctx, cacheDuration);
+
+            // Set the status code
+            ctx.Response.StatusCode = StatusCode;
+
+            if (StatusCode == 200 || StatusCode == 206)
+            {
+                // Finally, write the response data
+                Stream outputStream = ctx.Response.OutputStream;
+
+                if (CompressResponse && ClientSupportsCompression)
                 {
-                    CompressedStream = new DeflateStream(stream, CompressionLevel.Fastest, false);
+                    if (CompressionMethod.Equals("deflate", StringComparison.OrdinalIgnoreCase))
+                    {
+                        CompressedStream = new DeflateStream(outputStream, CompressionLevel.Fastest, false);
+                    }
+                    else
+                    {
+                        CompressedStream = new GZipStream(outputStream, CompressionLevel.Fastest, false);
+                    }
+
+                    outputStream = CompressedStream;
                 }
-                else
+
+                WriteResponseToOutputStream(outputStream);
+
+                if (!IsAsyncHandler)
                 {
-                    CompressedStream = new GZipStream(stream, CompressionLevel.Fastest, false);
+                    DisposeResponseStream();
                 }
-
-                WriteResponseToOutputStream(CompressedStream);
             }
             else
             {
-                WriteResponseToOutputStream(stream);
+                ctx.Response.SendChunked = false;
+                DisposeResponseStream();
             }
         }
 
+        protected virtual void PrepareUncachedResponse(HttpListenerContext ctx, TimeSpan cacheDuration)
+        {
+        }
+
+        private void CacheResponse(HttpListenerResponse response, TimeSpan duration, DateTime? dateModified)
+        {
+            DateTime lastModified = dateModified ?? DateTime.Now;
+
+            response.Headers[HttpResponseHeader.CacheControl] = "public, max-age=" + Convert.ToInt32(duration.TotalSeconds);
+            response.Headers[HttpResponseHeader.Expires] = DateTime.Now.Add(duration).ToString("r");
+            response.Headers[HttpResponseHeader.LastModified] = lastModified.ToString("r");
+        }
+
         protected abstract void WriteResponseToOutputStream(Stream stream);
 
         protected void DisposeResponseStream()
@@ -222,7 +337,45 @@ namespace MediaBrowser.Common.Net.Handlers
                 CompressedStream.Dispose();
             }
 
-            RequestContext.Response.OutputStream.Dispose();
+            HttpListenerContext.Response.OutputStream.Dispose();
+        }
+
+        private bool IsCacheValid(DateTime ifModifiedSince, TimeSpan cacheDuration, DateTime? dateModified)
+        {
+            if (dateModified.HasValue)
+            {
+                DateTime lastModified = NormalizeDateForComparison(dateModified.Value);
+                ifModifiedSince = NormalizeDateForComparison(ifModifiedSince);
+
+                return lastModified <= ifModifiedSince;
+            }
+
+            DateTime cacheExpirationDate = ifModifiedSince.Add(cacheDuration);
+
+            if (DateTime.Now < cacheExpirationDate)
+            {
+                return true;
+            }
+
+            return false;
+        }
+
+        /// <summary>
+        /// When the browser sends the IfModifiedDate, it's precision is limited to seconds, so this will account for that
+        /// </summary>
+        private DateTime NormalizeDateForComparison(DateTime date)
+        {
+            return new DateTime(date.Year, date.Month, date.Day, date.Hour, date.Minute, date.Second);
+        }
+
+        protected virtual long? GetTotalContentLength()
+        {
+            return null;
+        }
+
+        protected virtual DateTime? GetLastDateModified()
+        {
+            return null;
         }
     }
 }

+ 282 - 0
MediaBrowser.Common/Net/Handlers/StaticFileHandler.cs

@@ -0,0 +1,282 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Logging;
+
+namespace MediaBrowser.Common.Net.Handlers
+{
+    public class StaticFileHandler : BaseHandler
+    {
+        public string Path
+        {
+            get
+            {
+                return QueryString["path"];
+            }
+        }
+
+        private bool FileStreamDiscovered = false;
+        private FileStream _FileStream = null;
+        private FileStream FileStream
+        {
+            get
+            {
+                if (!FileStreamDiscovered)
+                {
+                    try
+                    {
+                        _FileStream = File.OpenRead(Path);
+                    }
+                    catch (FileNotFoundException)
+                    {
+                        StatusCode = 404;
+                    }
+                    catch (DirectoryNotFoundException)
+                    {
+                        StatusCode = 404;
+                    }
+                    catch (UnauthorizedAccessException)
+                    {
+                        StatusCode = 403;
+                    }
+                    finally
+                    {
+                        FileStreamDiscovered = true;
+                    }
+                }
+
+                return _FileStream;
+            }
+        }
+
+        protected override bool SupportsByteRangeRequests
+        {
+            get
+            {
+                return true;
+            }
+        }
+
+        public override bool CompressResponse
+        {
+            get
+            {
+                string contentType = ContentType;
+
+                // Can't compress these
+                if (IsRangeRequest)
+                {
+                    return false;
+                }
+
+                // Don't compress media
+                if (contentType.StartsWith("audio/", StringComparison.OrdinalIgnoreCase) || contentType.StartsWith("video/", StringComparison.OrdinalIgnoreCase))
+                {
+                    return false;
+                }
+
+                // It will take some work to support compression within this handler
+                return false;
+            }
+        }
+
+        protected override long? GetTotalContentLength()
+        {
+            try
+            {
+                return FileStream.Length;
+            }
+            catch
+            {
+                return base.GetTotalContentLength();
+            }
+        }
+
+        protected override DateTime? GetLastDateModified()
+        {
+            try
+            {
+                return File.GetLastWriteTime(Path);
+            }
+            catch
+            {
+                return base.GetLastDateModified();
+            }
+        }
+
+        protected override bool IsAsyncHandler
+        {
+            get
+            {
+                return true;
+            }
+        }
+
+        public override string ContentType
+        {
+            get
+            {
+                return MimeTypes.GetMimeType(Path);
+            }
+        }
+
+        protected async override void WriteResponseToOutputStream(Stream stream)
+        {
+            try
+            {
+                if (FileStream != null)
+                {
+                    if (IsRangeRequest)
+                    {
+                        KeyValuePair<long, long?> requestedRange = RequestedRanges.First();
+
+                        // If the requested range is "0-" and we know the total length, we can optimize by avoiding having to buffer the content into memory
+                        if (requestedRange.Value == null && TotalContentLength != null)
+                        {
+                            await ServeCompleteRangeRequest(requestedRange, stream);
+                        }
+                        else if (TotalContentLength.HasValue)
+                        {
+                            // This will have to buffer a portion of the content into memory
+                            await ServePartialRangeRequestWithKnownTotalContentLength(requestedRange, stream);
+                        }
+                        else
+                        {
+                            // This will have to buffer the entire content into memory
+                            await ServePartialRangeRequestWithUnknownTotalContentLength(requestedRange, stream);
+                        }
+                    }
+                    else
+                    {
+                        await FileStream.CopyToAsync(stream);
+                    }
+                }
+            }
+            catch (Exception ex)
+            {
+                Logger.LogException("WriteResponseToOutputStream", ex);
+            }
+            finally
+            {
+                if (FileStream != null)
+                {
+                    FileStream.Dispose();
+                }
+
+                DisposeResponseStream();
+            }
+        }
+
+        /// <summary>
+        /// Handles a range request of "bytes=0-"
+        /// This will serve the complete content and add the content-range header
+        /// </summary>
+        private async Task ServeCompleteRangeRequest(KeyValuePair<long, long?> requestedRange, Stream responseStream)
+        {
+            long totalContentLength = TotalContentLength.Value;
+
+            long rangeStart = requestedRange.Key;
+            long rangeEnd = totalContentLength - 1;
+            long rangeLength = 1 + rangeEnd - rangeStart;
+
+            // Content-Length is the length of what we're serving, not the original content
+            HttpListenerContext.Response.ContentLength64 = rangeLength;
+            HttpListenerContext.Response.Headers["Content-Range"] = string.Format("bytes {0}-{1}/{2}", rangeStart, rangeEnd, totalContentLength);
+
+            if (rangeStart > 0)
+            {
+                FileStream.Position = rangeStart;
+            }
+
+            await FileStream.CopyToAsync(responseStream);
+        }
+
+        /// <summary>
+        /// Serves a partial range request where the total content length is not known
+        /// </summary>
+        private async Task ServePartialRangeRequestWithUnknownTotalContentLength(KeyValuePair<long, long?> requestedRange, Stream responseStream)
+        {
+            // Read the entire stream so that we can determine the length
+            byte[] bytes = await ReadBytes(FileStream, 0, null);
+
+            long totalContentLength = bytes.LongLength;
+
+            long rangeStart = requestedRange.Key;
+            long rangeEnd = requestedRange.Value ?? (totalContentLength - 1);
+            long rangeLength = 1 + rangeEnd - rangeStart;
+
+            // Content-Length is the length of what we're serving, not the original content
+            HttpListenerContext.Response.ContentLength64 = rangeLength;
+            HttpListenerContext.Response.Headers["Content-Range"] = string.Format("bytes {0}-{1}/{2}", rangeStart, rangeEnd, totalContentLength);
+
+            await responseStream.WriteAsync(bytes, Convert.ToInt32(rangeStart), Convert.ToInt32(rangeLength));
+        }
+
+        /// <summary>
+        /// Serves a partial range request where the total content length is already known
+        /// </summary>
+        private async Task ServePartialRangeRequestWithKnownTotalContentLength(KeyValuePair<long, long?> requestedRange, Stream responseStream)
+        {
+            long totalContentLength = TotalContentLength.Value;
+            long rangeStart = requestedRange.Key;
+            long rangeEnd = requestedRange.Value ?? (totalContentLength - 1);
+            long rangeLength = 1 + rangeEnd - rangeStart;
+
+            // Only read the bytes we need
+            byte[] bytes = await ReadBytes(FileStream, Convert.ToInt32(rangeStart), Convert.ToInt32(rangeLength));
+
+            // Content-Length is the length of what we're serving, not the original content
+            HttpListenerContext.Response.ContentLength64 = rangeLength;
+
+            HttpListenerContext.Response.Headers["Content-Range"] = string.Format("bytes {0}-{1}/{2}", rangeStart, rangeEnd, totalContentLength);
+
+            await responseStream.WriteAsync(bytes, 0, Convert.ToInt32(rangeLength));
+        }
+
+        /// <summary>
+        /// Reads bytes from a stream
+        /// </summary>
+        /// <param name="input">The input stream</param>
+        /// <param name="start">The starting position</param>
+        /// <param name="count">The number of bytes to read, or null to read to the end.</param>
+        private async Task<byte[]> ReadBytes(Stream input, int start, int? count)
+        {
+            if (start > 0)
+            {
+                input.Position = start;
+            }
+
+            if (count == null)
+            {
+                byte[] buffer = new byte[16 * 1024];
+
+                using (MemoryStream ms = new MemoryStream())
+                {
+                    int read;
+                    while ((read = await input.ReadAsync(buffer, 0, buffer.Length)) > 0)
+                    {
+                        await ms.WriteAsync(buffer, 0, read);
+                    }
+                    return ms.ToArray();
+                }
+            }
+            else
+            {
+                byte[] buffer = new byte[count.Value];
+
+                using (MemoryStream ms = new MemoryStream())
+                {
+                    int read = await input.ReadAsync(buffer, 0, buffer.Length);
+
+                    await ms.WriteAsync(buffer, 0, read);
+
+                    return ms.ToArray();
+                }
+            }
+
+        }
+    }
+}

+ 5 - 6
MediaBrowser.Common/Net/HttpServer.cs

@@ -4,10 +4,10 @@ using System.Reactive.Linq;
 
 namespace MediaBrowser.Common.Net
 {
-    public class HttpServer : IObservable<RequestContext>, IDisposable
+    public class HttpServer : IObservable<HttpListenerContext>, IDisposable
     {
         private readonly HttpListener listener;
-        private readonly IObservable<RequestContext> stream;
+        private readonly IObservable<HttpListenerContext> stream;
 
         public HttpServer(string url)
         {
@@ -17,12 +17,11 @@ namespace MediaBrowser.Common.Net
             stream = ObservableHttpContext();
         }
 
-        private IObservable<RequestContext> ObservableHttpContext()
+        private IObservable<HttpListenerContext> ObservableHttpContext()
         {
-            return Observable.Create<RequestContext>(obs =>
+            return Observable.Create<HttpListenerContext>(obs =>
                                 Observable.FromAsyncPattern<HttpListenerContext>(listener.BeginGetContext,
                                                                                  listener.EndGetContext)()
-                                          .Select(c => new RequestContext(c))
                                           .Subscribe(obs))
                              .Repeat()
                              .Retry()
@@ -34,7 +33,7 @@ namespace MediaBrowser.Common.Net
             listener.Stop();
         }
 
-        public IDisposable Subscribe(IObserver<RequestContext> observer)
+        public IDisposable Subscribe(IObserver<HttpListenerContext> observer)
         {
             return stream.Subscribe(observer);
         }

+ 0 - 103
MediaBrowser.Common/Net/RequestContext.cs

@@ -1,103 +0,0 @@
-using System;
-using System.Linq;
-using System.Net;
-using MediaBrowser.Common.Logging;
-using MediaBrowser.Common.Net.Handlers;
-
-namespace MediaBrowser.Common.Net
-{
-    public class RequestContext
-    {
-        public HttpListenerRequest Request { get; private set; }
-        public HttpListenerResponse Response { get; private set; }
-
-        public string LocalPath
-        {
-            get
-            {
-                return Request.Url.LocalPath;
-            }
-        }
-
-        public RequestContext(HttpListenerContext context)
-        {
-            Response = context.Response;
-            Request = context.Request;
-        }
-
-        public void Respond(BaseHandler handler)
-        {
-            Logger.LogInfo("Http Server received request at: " + Request.Url.ToString());
-            Logger.LogInfo("Http Headers: " + string.Join(",", Request.Headers.AllKeys.Select(k => k + "=" + Request.Headers[k])));
-
-            Response.AddHeader("Access-Control-Allow-Origin", "*");
-
-            Response.KeepAlive = true;
-
-            foreach (var header in handler.Headers)
-            {
-                Response.AddHeader(header.Key, header.Value);
-            }
-
-            int statusCode = handler.StatusCode;
-            Response.ContentType = handler.ContentType;
-
-            TimeSpan cacheDuration = handler.CacheDuration;
-
-            if (Request.Headers.AllKeys.Contains("If-Modified-Since"))
-            {
-                DateTime ifModifiedSince;
-
-                if (DateTime.TryParse(Request.Headers["If-Modified-Since"].Replace(" GMT", string.Empty), out ifModifiedSince))
-                {
-                    // If the cache hasn't expired yet just return a 304
-                    if (IsCacheValid(ifModifiedSince, cacheDuration, handler.LastDateModified))
-                    {
-                        statusCode = 304;
-                    }
-                }
-            }
-
-            Response.StatusCode = statusCode;
-
-            if (statusCode == 200 || statusCode == 206)
-            {
-                handler.WriteStream(Response.OutputStream);
-            }
-            else
-            {
-                Response.SendChunked = false;
-                Response.OutputStream.Dispose();
-            }
-        }
-
-        private bool IsCacheValid(DateTime ifModifiedSince, TimeSpan cacheDuration, DateTime? dateModified)
-        {
-            if (dateModified.HasValue)
-            {
-                DateTime lastModified = NormalizeDateForComparison(dateModified.Value);
-                ifModifiedSince = NormalizeDateForComparison(ifModifiedSince);
-
-                return lastModified <= ifModifiedSince;
-            }
-
-            DateTime cacheExpirationDate = ifModifiedSince.Add(cacheDuration);
-
-            if (DateTime.Now < cacheExpirationDate)
-            {
-                return true;
-            }
-
-            return false;
-        }
-
-        /// <summary>
-        /// When the browser sends the IfModifiedDate, it's precision is limited to seconds, so this will account for that
-        /// </summary>
-        private DateTime NormalizeDateForComparison(DateTime date)
-        {
-            return new DateTime(date.Year, date.Month, date.Day, date.Hour, date.Minute, date.Second);
-        }
-
-    }
-}