Netcore WebApi中支持Range返回视频
背景
业务需要,通过WebApi返回视频,起初,直接在Controller读取视频信息,返回FileContentResult。大部分浏览器也能正常播放。但是会存在以下问题:
- Mac系统下safari浏览器播放视频失败。
- Html Video标签下,视频无法拖动进度,一些类似进度跳转,加载到最近一次播放进度的功能需求就无法满足。
- 视频加载缓慢,wpf或其他客户端下,一些大视频可能会加载很长时间才能播放,而且播放卡顿严重。
原因
Mac 系统Safari浏览器在加载视频是要求视频必须支持分块加载,即必须支持Range请求头,而我们直接返回FileContentResult是不支持Range的。
同样,Video标签下不能拖动进度和客户端加载视频缓慢也是这个原因。
解决
找到原因,解决问题就相对简单了,google了一番,加上GitHub搜索,找到如下方法。废话不多说,直接上代码。
VideoStreamResult类
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
public class VideoStreamResult : FileStreamResult
{
// default buffer size as defined in BufferedStream type
private const int BufferSize = 0x1000;
private string MultipartBoundary = "<qwe123>";
public VideoStreamResult(Stream fileStream, string contentType)
: base(fileStream, contentType)
{
}
public VideoStreamResult(Stream fileStream, MediaTypeHeaderValue contentType)
: base(fileStream, contentType)
{
}
private bool IsMultipartRequest(RangeHeaderValue range)
{
return range != null && range.Ranges != null && range.Ranges.Count > 1;
}
private bool IsRangeRequest(RangeHeaderValue range)
{
return range != null && range.Ranges != null && range.Ranges.Count > 0;
}
protected async Task WriteVideoAsync(HttpResponse response)
{
var bufferingFeature = response.HttpContext.Features.Get<IHttpResponseBodyFeature>();
bufferingFeature?.DisableBuffering();
var length = FileStream.Length;
var range = response.HttpContext.GetRanges(length);
if (IsMultipartRequest(range))
{
response.ContentType = $"multipart/byteranges; boundary={MultipartBoundary}";
}
else
{
response.ContentType = ContentType.ToString();
}
response.Headers.Add("Accept-Ranges", "bytes");
if (IsRangeRequest(range))
{
response.StatusCode = (int)HttpStatusCode.PartialContent;
if (!IsMultipartRequest(range))
{
response.Headers.Add("Content-Range", $"bytes {range.Ranges.First().From}-{range.Ranges.First().To}/{length}");
}
foreach (var rangeValue in range.Ranges)
{
if (IsMultipartRequest(range)) // dunno if multipart works
{
await response.WriteAsync($"--{MultipartBoundary}");
await response.WriteAsync(Environment.NewLine);
await response.WriteAsync($"Content-type: {ContentType}");
await response.WriteAsync(Environment.NewLine);
await response.WriteAsync($"Content-Range: bytes {range.Ranges.First().From}-{range.Ranges.First().To}/{length}");
await response.WriteAsync(Environment.NewLine);
}
await WriteDataToResponseBody(rangeValue, response);
if (IsMultipartRequest(range))
{
await response.WriteAsync(Environment.NewLine);
}
}
if (IsMultipartRequest(range))
{
await response.WriteAsync($"--{MultipartBoundary}--");
await response.WriteAsync(Environment.NewLine);
}
}
else
{
await FileStream.CopyToAsync(response.Body);
}
}
private async Task WriteDataToResponseBody(RangeItemHeaderValue rangeValue, HttpResponse response)
{
var startIndex = rangeValue.From ?? 0;
var endIndex = rangeValue.To ?? 0;
byte[] buffer = new byte[BufferSize];
long totalToSend = endIndex - startIndex;
int count = 0;
long bytesRemaining = totalToSend + 1;
response.ContentLength = bytesRemaining;
FileStream.Seek(startIndex, SeekOrigin.Begin);
while (bytesRemaining > 0)
{
try
{
if (bytesRemaining <= buffer.Length)
count = FileStream.Read(buffer, 0, (int)bytesRemaining);
else
count = FileStream.Read(buffer, 0, buffer.Length);
if (count == 0)
return;
await response.Body.WriteAsync(buffer, 0, count);
bytesRemaining -= count;
}
catch (IndexOutOfRangeException)
{
await response.Body.FlushAsync();
return;
}
finally
{
await response.Body.FlushAsync();
}
}
}
public override async Task ExecuteResultAsync(ActionContext context)
{
await WriteVideoAsync(context.HttpContext.Response);
}
}
这里使用了一个扩展方法:
public static RangeHeaderValue GetRanges(this HttpContext context, long contentSize)
{
RangeHeaderValue rangesResult = null;
string rangeHeader = context.Request.Headers["Range"];
if (!string.IsNullOrEmpty(rangeHeader))
{
string[] ranges = rangeHeader.Replace("bytes=", string.Empty).Split(",".ToCharArray());
rangesResult = new RangeHeaderValue();
for (int i = 0; i < ranges.Length; i++)
{
const int START = 0, END = 1;
long endByte, startByte;
long parsedValue;
string[] currentRange = ranges[i].Split("-".ToCharArray());
if (long.TryParse(currentRange[END], out parsedValue))
endByte = parsedValue;
else
endByte = contentSize - 1;
if (long.TryParse(currentRange[START], out parsedValue))
startByte = parsedValue;
else
{
startByte = contentSize - endByte;
endByte = contentSize - 1;
}
rangesResult.Ranges.Add(new RangeItemHeaderValue(startByte, endByte));
}
}
return rangesResult;
}
在Controller中使用:
public async Task<ActionResult> DownloadById(string filepath)
{
///根据业务,获取文件Stream
var stream = new FileStream(filepath, FileMode.Open, FileAccess.Read);
return new VideoStreamResult(stream, mimeType);
}
似乎并不能完美的解决MAC和IOS系统下的播放,请问还有哪方面的影响,只要求针对IOS12以上的版本即可。
我这边按这种方式处理后,让前端试了下,Mac系统下safari浏览器可以正常播放了。IOS系统我并未进行测试。按说,只要实现了这种视频流形式。应该是可以播放的。具体到前端实现我就不太懂了。