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

pull/702/head
LukePulverenti Luke Pulverenti luke pulverenti 12 years ago
parent dce7706382
commit 2536011247

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

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

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

@ -3,32 +3,36 @@ using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.IO; using System.IO;
using System.IO.Compression; using System.IO.Compression;
using System.Linq;
using System.Net; using System.Net;
using MediaBrowser.Common.Logging;
namespace MediaBrowser.Common.Net.Handlers namespace MediaBrowser.Common.Net.Handlers
{ {
public abstract class BaseHandler public abstract class BaseHandler
{ {
/// <summary>
/// Response headers
/// </summary>
public IDictionary<string, string> Headers = new Dictionary<string, string>();
private Stream CompressedStream { get; set; } private Stream CompressedStream { get; set; }
public virtual bool UseChunkedEncoding public virtual bool? UseChunkedEncoding
{ {
get get
{ {
return true; return null;
} }
} }
public virtual long? ContentLength private bool _TotalContentLengthDiscovered = false;
private long? _TotalContentLength = null;
public long? TotalContentLength
{ {
get get
{ {
return null; if (!_TotalContentLengthDiscovered)
{
_TotalContentLength = GetTotalContentLength();
}
return _TotalContentLength;
} }
} }
@ -44,29 +48,18 @@ namespace MediaBrowser.Common.Net.Handlers
} }
} }
/// <summary> protected virtual bool SupportsByteRangeRequests
/// The action to write the response to the output stream
/// </summary>
public Action<Stream> WriteStream
{ {
get get
{ {
return s => return false;
{
WriteReponse(s);
if (!IsAsyncHandler)
{
DisposeResponseStream();
}
};
} }
} }
/// <summary> /// <summary>
/// The original RequestContext /// The original HttpListenerContext
/// </summary> /// </summary>
public RequestContext RequestContext { get; set; } protected HttpListenerContext HttpListenerContext { get; private set; }
/// <summary> /// <summary>
/// The original QueryString /// The original QueryString
@ -75,7 +68,54 @@ namespace MediaBrowser.Common.Net.Handlers
{ {
get 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> /// <summary>
/// Gets the status code to include in the response headers /// Gets the status code to include in the response headers
/// </summary> /// </summary>
public virtual int StatusCode protected int StatusCode { get; set; }
{
get
{
return 200;
}
}
/// <summary> /// <summary>
/// Gets the cache duration to include in the response headers /// 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> /// <summary>
/// Gets the last date modified of the content being returned, if this can be determined. /// 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. /// This will be used to invalidate the cache, so it's not needed if CacheDuration is 0.
/// </summary> /// </summary>
public virtual DateTime? LastDateModified public DateTime? LastDateModified
{ {
get get
{ {
return null; if (!_LastDateModifiedDiscovered)
{
_LastDateModified = GetLastDateModified();
}
return _LastDateModified;
} }
} }
public virtual bool CompressResponse public virtual bool CompressResponse
{ {
get get
@ -130,7 +171,7 @@ namespace MediaBrowser.Common.Net.Handlers
{ {
get 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; return enc.IndexOf("deflate", StringComparison.OrdinalIgnoreCase) != -1 || enc.IndexOf("gzip", StringComparison.OrdinalIgnoreCase) != -1;
} }
@ -140,7 +181,7 @@ namespace MediaBrowser.Common.Net.Handlers
{ {
get 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) 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. HttpListenerContext = ctx;
if (!UseChunkedEncoding)
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);
} }
else
TimeSpan cacheDuration = CacheDuration;
if (cacheDuration.Ticks > 0)
{ {
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); // By default, use chunked encoding if we don't know the content length
response.Headers[HttpResponseHeader.Expires] = DateTime.Now.Add(duration).ToString("r"); bool useChunkedEncoding = UseChunkedEncoding == null ? (totalContentLength == null) : UseChunkedEncoding.Value;
response.Headers[HttpResponseHeader.LastModified] = lastModified.ToString("r");
}
private void WriteReponse(Stream stream) // Don't force this to true. HttpListener will default it to true if supported by the client.
{ if (!useChunkedEncoding)
PrepareResponseBeforeWriteOutput(RequestContext.Response); {
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 (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 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 abstract void WriteResponseToOutputStream(Stream stream);
protected void DisposeResponseStream() protected void DisposeResponseStream()
@ -222,7 +337,45 @@ namespace MediaBrowser.Common.Net.Handlers
CompressedStream.Dispose(); 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;
} }
} }
} }

@ -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();
}
}
}
}
}

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

@ -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);
}
}
}
Loading…
Cancel
Save