From e5592bd220f09a85314cd56fb9c5a287061b9752 Mon Sep 17 00:00:00 2001 From: LukePulverenti Date: Sun, 10 Mar 2013 23:12:21 -0400 Subject: bring back support for byte ranged requests --- .../HttpServer/BaseRestService.cs | 12 +- .../HttpServer/RangeRequestWriter.cs | 172 +++++++++++++++++++++ 2 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 MediaBrowser.Server.Implementations/HttpServer/RangeRequestWriter.cs (limited to 'MediaBrowser.Server.Implementations/HttpServer') diff --git a/MediaBrowser.Server.Implementations/HttpServer/BaseRestService.cs b/MediaBrowser.Server.Implementations/HttpServer/BaseRestService.cs index cdb6adbe7..89d1ea72d 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/BaseRestService.cs +++ b/MediaBrowser.Server.Implementations/HttpServer/BaseRestService.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Common.Extensions; +using System.Net; +using MediaBrowser.Common.Extensions; using MediaBrowser.Common.IO; using MediaBrowser.Common.Net; using MediaBrowser.Model.Logging; @@ -257,6 +258,15 @@ namespace MediaBrowser.Server.Implementations.HttpServer var stream = await factoryFn().ConfigureAwait(false); + var httpListenerResponse = (HttpListenerResponse) Response.OriginalResponse; + httpListenerResponse.SendChunked = false; + + if (IsRangeRequest) + { + return new RangeRequestWriter(Request.Headers, httpListenerResponse, stream); + } + + httpListenerResponse.ContentLength64 = stream.Length; return new StreamWriter(stream); } diff --git a/MediaBrowser.Server.Implementations/HttpServer/RangeRequestWriter.cs b/MediaBrowser.Server.Implementations/HttpServer/RangeRequestWriter.cs new file mode 100644 index 000000000..77e14362d --- /dev/null +++ b/MediaBrowser.Server.Implementations/HttpServer/RangeRequestWriter.cs @@ -0,0 +1,172 @@ +using ServiceStack.Service; +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.IO; +using System.Linq; +using System.Net; +using System.Threading.Tasks; + +namespace MediaBrowser.Server.Implementations.HttpServer +{ + public class RangeRequestWriter : IStreamWriter + { + /// + /// Gets or sets the source stream. + /// + /// The source stream. + public Stream SourceStream { get; set; } + public HttpListenerResponse Response { get; set; } + public NameValueCollection RequestHeaders { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The request headers. + /// The response. + /// The source. + public RangeRequestWriter(NameValueCollection requestHeaders, HttpListenerResponse response, Stream source) + { + RequestHeaders = requestHeaders; + Response = response; + SourceStream = source; + } + + /// + /// The _requested ranges + /// + private List> _requestedRanges; + /// + /// Gets the requested ranges. + /// + /// The requested ranges. + protected IEnumerable> RequestedRanges + { + get + { + if (_requestedRanges == null) + { + _requestedRanges = new List>(); + + // Example: bytes=0-,32-63 + var ranges = RequestHeaders["Range"].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]); + } + if (!string.IsNullOrEmpty(vals[1])) + { + end = long.Parse(vals[1]); + } + + _requestedRanges.Add(new KeyValuePair(start, end)); + } + } + + return _requestedRanges; + } + } + + /// + /// Writes to. + /// + /// The response stream. + public void WriteTo(Stream responseStream) + { + Response.Headers["Accept-Ranges"] = "bytes"; + Response.StatusCode = 206; + + var task = WriteToAsync(responseStream); + + Task.WaitAll(task); + } + + /// + /// Writes to async. + /// + /// The response stream. + /// Task. + private Task WriteToAsync(Stream responseStream) + { + var requestedRange = RequestedRanges.First(); + + var totalLength = SourceStream.Length; + + // If the requested range is "0-", we can optimize by just doing a stream copy + if (!requestedRange.Value.HasValue) + { + return ServeCompleteRangeRequest(requestedRange, responseStream, totalLength); + } + + // This will have to buffer a portion of the content into memory + return ServePartialRangeRequest(requestedRange.Key, requestedRange.Value.Value, responseStream, totalLength); + } + + /// + /// Handles a range request of "bytes=0-" + /// This will serve the complete content and add the content-range header + /// + /// The requested range. + /// The response stream. + /// Total length of the content. + /// Task. + private Task ServeCompleteRangeRequest(KeyValuePair requestedRange, Stream responseStream, long totalContentLength) + { + var rangeStart = requestedRange.Key; + var rangeEnd = totalContentLength - 1; + var rangeLength = 1 + rangeEnd - rangeStart; + + // Content-Length is the length of what we're serving, not the original content + Response.ContentLength64 = rangeLength; + Response.Headers["Content-Range"] = string.Format("bytes {0}-{1}/{2}", rangeStart, rangeEnd, totalContentLength); + + if (rangeStart > 0) + { + SourceStream.Position = rangeStart; + } + + return SourceStream.CopyToAsync(responseStream); + } + + /// + /// Serves a partial range request + /// + /// The range start. + /// The range end. + /// The response stream. + /// Total length of the content. + /// Task. + private async Task ServePartialRangeRequest(long rangeStart, long rangeEnd, Stream responseStream, long totalContentLength) + { + var rangeLength = 1 + rangeEnd - rangeStart; + + // Content-Length is the length of what we're serving, not the original content + Response.ContentLength64 = rangeLength; + Response.Headers["Content-Range"] = string.Format("bytes {0}-{1}/{2}", rangeStart, rangeEnd, totalContentLength); + + SourceStream.Position = rangeStart; + + // Fast track to just copy the stream to the end + if (rangeEnd == totalContentLength - 1) + { + await SourceStream.CopyToAsync(responseStream).ConfigureAwait(false); + } + else + { + // Read the bytes we need + var buffer = new byte[Convert.ToInt32(rangeLength)]; + await SourceStream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); + + await responseStream.WriteAsync(buffer, 0, Convert.ToInt32(rangeLength)).ConfigureAwait(false); + } + } + } +} -- cgit v1.2.3 From a96ec4f169f82d40595fbb0d8f40ccc90c1074e9 Mon Sep 17 00:00:00 2001 From: LukePulverenti Date: Sun, 10 Mar 2013 23:17:36 -0400 Subject: better source stream disposal --- .../HttpServer/RangeRequestWriter.cs | 39 ++++++++++++---------- 1 file changed, 22 insertions(+), 17 deletions(-) (limited to 'MediaBrowser.Server.Implementations/HttpServer') diff --git a/MediaBrowser.Server.Implementations/HttpServer/RangeRequestWriter.cs b/MediaBrowser.Server.Implementations/HttpServer/RangeRequestWriter.cs index 77e14362d..e5c4c9796 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/RangeRequestWriter.cs +++ b/MediaBrowser.Server.Implementations/HttpServer/RangeRequestWriter.cs @@ -94,31 +94,35 @@ namespace MediaBrowser.Server.Implementations.HttpServer /// /// The response stream. /// Task. - private Task WriteToAsync(Stream responseStream) + private async Task WriteToAsync(Stream responseStream) { - var requestedRange = RequestedRanges.First(); + using (var source = SourceStream) + { + var requestedRange = RequestedRanges.First(); - var totalLength = SourceStream.Length; + var totalLength = SourceStream.Length; - // If the requested range is "0-", we can optimize by just doing a stream copy - if (!requestedRange.Value.HasValue) - { - return ServeCompleteRangeRequest(requestedRange, responseStream, totalLength); - } + // If the requested range is "0-", we can optimize by just doing a stream copy + if (!requestedRange.Value.HasValue) + { + await ServeCompleteRangeRequest(source, requestedRange, responseStream, totalLength).ConfigureAwait(false); + } - // This will have to buffer a portion of the content into memory - return ServePartialRangeRequest(requestedRange.Key, requestedRange.Value.Value, responseStream, totalLength); + // This will have to buffer a portion of the content into memory + await ServePartialRangeRequest(source, requestedRange.Key, requestedRange.Value.Value, responseStream, totalLength).ConfigureAwait(false); + } } /// /// Handles a range request of "bytes=0-" /// This will serve the complete content and add the content-range header /// + /// The source stream. /// The requested range. /// The response stream. /// Total length of the content. /// Task. - private Task ServeCompleteRangeRequest(KeyValuePair requestedRange, Stream responseStream, long totalContentLength) + private Task ServeCompleteRangeRequest(Stream sourceStream, KeyValuePair requestedRange, Stream responseStream, long totalContentLength) { var rangeStart = requestedRange.Key; var rangeEnd = totalContentLength - 1; @@ -130,21 +134,22 @@ namespace MediaBrowser.Server.Implementations.HttpServer if (rangeStart > 0) { - SourceStream.Position = rangeStart; + sourceStream.Position = rangeStart; } - return SourceStream.CopyToAsync(responseStream); + return sourceStream.CopyToAsync(responseStream); } /// /// Serves a partial range request /// + /// The source stream. /// The range start. /// The range end. /// The response stream. /// Total length of the content. /// Task. - private async Task ServePartialRangeRequest(long rangeStart, long rangeEnd, Stream responseStream, long totalContentLength) + private async Task ServePartialRangeRequest(Stream sourceStream, long rangeStart, long rangeEnd, Stream responseStream, long totalContentLength) { var rangeLength = 1 + rangeEnd - rangeStart; @@ -152,18 +157,18 @@ namespace MediaBrowser.Server.Implementations.HttpServer Response.ContentLength64 = rangeLength; Response.Headers["Content-Range"] = string.Format("bytes {0}-{1}/{2}", rangeStart, rangeEnd, totalContentLength); - SourceStream.Position = rangeStart; + sourceStream.Position = rangeStart; // Fast track to just copy the stream to the end if (rangeEnd == totalContentLength - 1) { - await SourceStream.CopyToAsync(responseStream).ConfigureAwait(false); + await sourceStream.CopyToAsync(responseStream).ConfigureAwait(false); } else { // Read the bytes we need var buffer = new byte[Convert.ToInt32(rangeLength)]; - await SourceStream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); + await sourceStream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); await responseStream.WriteAsync(buffer, 0, Convert.ToInt32(rangeLength)).ConfigureAwait(false); } -- cgit v1.2.3 From 7c677e0ee6e9e8b7816a7dae9cbf1c55502c3599 Mon Sep 17 00:00:00 2001 From: LukePulverenti Date: Mon, 11 Mar 2013 00:04:08 -0400 Subject: better streaming logging --- .../Progressive/BaseProgressiveStreamingService.cs | 6 +----- .../Progressive/ProgressiveStreamWriter.cs | 21 +++++++++++++++++--- .../HttpServer/BaseRestService.cs | 2 +- .../HttpServer/StreamWriter.cs | 23 ++++++++++++++++++---- 4 files changed, 39 insertions(+), 13 deletions(-) (limited to 'MediaBrowser.Server.Implementations/HttpServer') diff --git a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs index 251cd4bd6..a4acc2845 100644 --- a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs +++ b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs @@ -135,11 +135,7 @@ namespace MediaBrowser.Api.Playback.Progressive ApiEntryPoint.Instance.OnTranscodeBeginRequest(outputPath, TranscodingJobType.Progressive); } - return new ProgressiveStreamWriter - { - Path = outputPath, - State = state - }; + return new ProgressiveStreamWriter(outputPath, state, Logger); } /// diff --git a/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs b/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs index e9e134002..efab3bbc6 100644 --- a/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs +++ b/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs @@ -9,9 +9,22 @@ namespace MediaBrowser.Api.Playback.Progressive { public class ProgressiveStreamWriter : IStreamWriter { - public string Path { get; set; } - public StreamState State { get; set; } - public ILogger Logger { get; set; } + private string Path { get; set; } + private StreamState State { get; set; } + private ILogger Logger { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The path. + /// The state. + /// The logger. + public ProgressiveStreamWriter(string path, StreamState state, ILogger logger) + { + Path = path; + State = state; + Logger = logger; + } /// /// Writes to. @@ -38,6 +51,8 @@ namespace MediaBrowser.Api.Playback.Progressive catch (Exception ex) { Logger.ErrorException("Error streaming media", ex); + + throw; } finally { diff --git a/MediaBrowser.Server.Implementations/HttpServer/BaseRestService.cs b/MediaBrowser.Server.Implementations/HttpServer/BaseRestService.cs index 89d1ea72d..0445cd863 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/BaseRestService.cs +++ b/MediaBrowser.Server.Implementations/HttpServer/BaseRestService.cs @@ -267,7 +267,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer } httpListenerResponse.ContentLength64 = stream.Length; - return new StreamWriter(stream); + return new StreamWriter(stream, Logger); } string content; diff --git a/MediaBrowser.Server.Implementations/HttpServer/StreamWriter.cs b/MediaBrowser.Server.Implementations/HttpServer/StreamWriter.cs index ab178b6ea..6f5d6e25f 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/StreamWriter.cs +++ b/MediaBrowser.Server.Implementations/HttpServer/StreamWriter.cs @@ -1,4 +1,6 @@ -using ServiceStack.Service; +using MediaBrowser.Model.Logging; +using ServiceStack.Service; +using System; using System.IO; using System.Threading.Tasks; @@ -9,6 +11,8 @@ namespace MediaBrowser.Server.Implementations.HttpServer /// public class StreamWriter : IStreamWriter { + private ILogger Logger { get; set; } + /// /// Gets or sets the source stream. /// @@ -19,9 +23,11 @@ namespace MediaBrowser.Server.Implementations.HttpServer /// Initializes a new instance of the class. /// /// The source. - public StreamWriter(Stream source) + /// The logger. + public StreamWriter(Stream source, ILogger logger) { SourceStream = source; + Logger = logger; } /// @@ -42,9 +48,18 @@ namespace MediaBrowser.Server.Implementations.HttpServer /// Task. private async Task WriteToAsync(Stream responseStream) { - using (var src = SourceStream) + try { - await src.CopyToAsync(responseStream).ConfigureAwait(false); + using (var src = SourceStream) + { + await src.CopyToAsync(responseStream).ConfigureAwait(false); + } + } + catch (Exception ex) + { + Logger.ErrorException("Error streaming media", ex); + + throw; } } } -- cgit v1.2.3