Skip to content

Instantly share code, notes, and snippets.

@alfeg
Last active November 8, 2018 20:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alfeg/668d2547ee8b8a6241c4dc1abfa39c4e to your computer and use it in GitHub Desktop.
Save alfeg/668d2547ee8b8a6241c4dc1abfa39c4e to your computer and use it in GitHub Desktop.
PartialFileContentResult to return 206 partial responses from MVC 5
public class PartialFileContentResult : ActionResult
{
private readonly Stream stream;
private readonly string contentType;
private readonly CancellationToken token;
/// <summary>Initializes a new instance of the <see cref="T:System.Web.Mvc.FileContentResult" /> class by using the specified file contents and content type.</summary>
/// <param name="fileContents">The byte array to send to the response.</param>
/// <param name="contentType">The content type to use for the response.</param>
/// <exception cref="T:System.ArgumentNullException">The <paramref name="fileContents" /> parameter is null.</exception>
public PartialFileContentResult(Stream stream, string contentType, CancellationToken token)
{
this.stream = stream;
this.contentType = contentType;
this.token = token;
}
public override void ExecuteResult(ControllerContext ctx)
{
var req = ctx.HttpContext.Request;
var rangeHeader = req.Headers["Range"];
if (rangeHeader != null)
{
if (RangeHelpers.TryParseRanges(rangeHeader, out var ranges))
{
var byteRange = new ByteRangeStreamContent(stream, new RangeHeaderValue(ranges[0].from, ranges[0].to),MediaTypeHeaderValue.Parse(contentType));
var response = ctx.HttpContext.Response;
response.StatusCode = 206;
response.ContentType = byteRange.Headers.ContentType.ToString();
response.AddHeader("Content-Length", byteRange.Headers.ContentLength.ToString());
response.AddHeader("Content-Range", byteRange.Headers.ContentRange.ToString());
using (this.stream)
{
byte[] buffer = new byte[4096];
stream.Seek(ranges[0].Item1 ?? 0, SeekOrigin.Begin);
while (true)
{
token.ThrowIfCancellationRequested();
if (!HttpContext.Current.Response.IsClientConnected)
{
return;
}
long bufferSize = 4096;
if (ranges[0].to != null)
{
bufferSize = Math.Min(bufferSize, ranges[0].to.Value - stream.Position);
}
int count = this.stream.Read(buffer, 0, (int) bufferSize);
if (count != 0)
response.OutputStream.Write(buffer, 0, count);
else
break;
}
}
return;
}
}
new FileStreamResult(stream, contentType).ExecuteResult(ctx);
}
}
internal static class RangeHelpers
{
// Examples:
// bytes=0-499
// bytes=500-
// bytes=-500
// bytes=0-0,-1
// bytes=500-600,601-999
// Any individual bad range fails the whole parse and the header should be ignored.
internal static bool TryParseRanges(string rangeHeader, out IList<(long? from, long? to)> parsedRanges)
{
parsedRanges = null;
if (string.IsNullOrWhiteSpace(rangeHeader)
|| !rangeHeader.StartsWith("bytes=", StringComparison.OrdinalIgnoreCase))
{
return false;
}
string[] subRanges = rangeHeader.Substring("bytes=".Length).Replace(" ", string.Empty).Split(',');
var ranges = new List<(long? from, long? to)>();
for (int i = 0; i < subRanges.Length; i++)
{
long? first = null, second = null;
string subRange = subRanges[i];
int dashIndex = subRange.IndexOf('-');
if (dashIndex < 0)
{
return false;
}
else if (dashIndex == 0)
{
// -500
string remainder = subRange.Substring(1);
if (!TryParseLong(remainder, out second))
{
return false;
}
}
else if (dashIndex == (subRange.Length - 1))
{
// 500-
string remainder = subRange.Substring(0, subRange.Length - 1);
if (!TryParseLong(remainder, out first))
{
return false;
}
}
else
{
// 0-499
string firstString = subRange.Substring(0, dashIndex);
string secondString = subRange.Substring(dashIndex + 1, subRange.Length - dashIndex - 1);
if (!TryParseLong(firstString, out first) || !TryParseLong(secondString, out second)
|| first.Value > second.Value)
{
return false;
}
}
ranges.Add((first, second));
}
if (ranges.Count > 0)
{
parsedRanges = ranges;
return true;
}
return false;
}
private static bool TryParseLong(string input, out long? result)
{
if (!string.IsNullOrWhiteSpace(input)
&& int.TryParse(input, NumberStyles.None, CultureInfo.InvariantCulture, out var temp))
{
result = temp;
return true;
}
result = null;
return false;
}
// 14.35.1 Byte Ranges - If a syntactically valid byte-range-set includes at least one byte-range-spec whose
// first-byte-pos is less than the current length of the entity-body, or at least one suffix-byte-range-spec
// with a non-zero suffix-length, then the byte-range-set is satisfiable.
// Adjusts ranges to be absolute and within bounds.
internal static IList<Tuple<long, long>> NormalizeRanges(IList<Tuple<long?, long?>> ranges, long length)
{
IList<Tuple<long, long>> normalizedRanges = new List<Tuple<long, long>>(ranges.Count);
for (int i = 0; i < ranges.Count; i++)
{
Tuple<long?, long?> range = ranges[i];
long? start = range.Item1, end = range.Item2;
// X-[Y]
if (start.HasValue)
{
if (start.Value >= length)
{
// Not satisfiable, skip/discard.
continue;
}
if (!end.HasValue || end.Value >= length)
{
end = length - 1;
}
}
else
{
// suffix range "-X" e.g. the last X bytes, resolve
if (end.Value == 0)
{
// Not satisfiable, skip/discard.
continue;
}
long bytes = Math.Min(end.Value, length);
start = length - bytes;
end = start + bytes - 1;
}
normalizedRanges.Add(new Tuple<long, long>(start.Value, end.Value));
}
return normalizedRanges;
}
}
new PartialFileContentResult(fileStream, "video/mp4", HttpContext.Response.ClientDisconnectedToken)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment