using ServiceStack.Service; using ServiceStack.ServiceHost; using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Net; using System.Threading.Tasks; namespace MediaBrowser.Server.Implementations.HttpServer { public class RangeRequestWriter : IStreamWriter, IHttpResult { /// /// Gets or sets the source stream. /// /// The source stream. private Stream SourceStream { 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; } /// /// The _options /// private readonly Dictionary _options = new Dictionary(); /// /// The us culture /// private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); /// /// Additional HTTP Headers /// /// The headers. public Dictionary Headers { get { return _options; } } /// /// Gets the options. /// /// The options. public IDictionary Options { get { return Headers; } } /// /// Initializes a new instance of the class. /// /// The range header. /// The source. /// Type of the content. /// if set to true [is head request]. public RangeRequestWriter(string rangeHeader, Stream source, string contentType, bool isHeadRequest) { if (string.IsNullOrEmpty(contentType)) { throw new ArgumentNullException("contentType"); } RangeHeader = rangeHeader; SourceStream = source; IsHeadRequest = isHeadRequest; ContentType = contentType; Options["Content-Type"] = contentType; Options["Accept-Ranges"] = "bytes"; StatusCode = HttpStatusCode.PartialContent; SetRangeValues(); } /// /// Sets the range values. /// private void SetRangeValues() { var requestedRange = RequestedRanges.First(); TotalContentLength = SourceStream.Length; // 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 Options["Content-Length"] = RangeLength.ToString(UsCulture); Options["Content-Range"] = string.Format("bytes {0}-{1}/{2}", RangeStart, RangeEnd, TotalContentLength); if (RangeStart > 0) { SourceStream.Position = RangeStart; } } /// /// The _requested ranges /// private List> _requestedRanges; /// /// Gets the requested ranges. /// /// The requested ranges. protected List> RequestedRanges { get { if (_requestedRanges == null) { _requestedRanges = new List>(); // 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(start, end)); } } return _requestedRanges; } } /// /// Writes to. /// /// The response stream. public void WriteTo(Stream responseStream) { var task = WriteToAsync(responseStream); Task.WaitAll(task); } /// /// Writes to async. /// /// The response stream. /// Task. private async Task WriteToAsync(Stream responseStream) { // Headers only if (IsHeadRequest) { return; } using (var source = SourceStream) { // If the requested range is "0-", we can optimize by just doing a stream copy if (RangeEnd == TotalContentLength - 1) { await source.CopyToAsync(responseStream).ConfigureAwait(false); } else { // Read the bytes we need var buffer = new byte[Convert.ToInt32(RangeLength)]; await source.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); await responseStream.WriteAsync(buffer, 0, Convert.ToInt32(RangeLength)).ConfigureAwait(false); } } } public string ContentType { get; set; } public IRequestContext RequestContext { get; set; } public object Response { get; set; } public IContentTypeWriter ResponseFilter { get; set; } public int Status { get; set; } public HttpStatusCode StatusCode { get { return (HttpStatusCode)Status; } set { Status = (int)value; } } public string StatusDescription { get; set; } } }