using MediaBrowser.Model.Logging; using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Net; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Model.Services; namespace Emby.Server.Implementations.HttpServer { public class RangeRequestWriter : IAsyncStreamWriter, 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; } public Action OnComplete { get; set; } private readonly ILogger _logger; private const int BufferSize = 81920; /// /// The _options /// private readonly Dictionary _options = new Dictionary(); /// /// The us culture /// private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); public List Cookies { get; private set; } /// /// Additional HTTP Headers /// /// The headers. public IDictionary Headers { get { return _options; } } /// /// 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, ILogger logger) { if (string.IsNullOrEmpty(contentType)) { throw new ArgumentNullException("contentType"); } RangeHeader = rangeHeader; SourceStream = source; IsHeadRequest = isHeadRequest; this._logger = logger; ContentType = contentType; Headers["Content-Type"] = contentType; Headers["Accept-Ranges"] = "bytes"; StatusCode = HttpStatusCode.PartialContent; Cookies = new List(); SetRangeValues(); } /// /// Sets the range values. /// private void SetRangeValues() { var requestedRange = RequestedRanges[0]; 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 Headers["Content-Length"] = RangeLength.ToString(UsCulture); Headers["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; } } public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken) { try { // 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, BufferSize).ConfigureAwait(false); } else { await CopyToInternalAsync(source, responseStream, RangeLength).ConfigureAwait(false); } } } finally { if (OnComplete != null) { OnComplete(); } } } private async Task CopyToInternalAsync(Stream source, Stream destination, long copyLength) { var array = new byte[BufferSize]; int count; while ((count = await source.ReadAsync(array, 0, array.Length).ConfigureAwait(false)) != 0) { var bytesToCopy = Math.Min(count, copyLength); await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToCopy)).ConfigureAwait(false); copyLength -= bytesToCopy; if (copyLength <= 0) { break; } } } 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; } } }