HTML5 video does not work like streaming technologies or Flash. So how does the browser manage to play a long video without downloading the whole file before playing it? Part of the trick is that the video is encoded in such a way that the metadata is at the top of the file. This means once the first chunk of data is returned it is enough to determine whether the browser can play it all the way through. If you are encoding video for use with the video element, you will want to choose the Web optimized option in the encoding software. (See the section on video encoding above.)
video 的metadata 被放到了文件头部,这样可以不用加载整个文件就能判断出浏览器是否播放这个video。
The real trick though is how Web servers allow you to only download the a part of a file you request. The browser requests a chunk of the video at a time which allows HTML5 video to give the appearance of streaming. This behavior of mimicking streaming is called progressive download. This also allows fast seeking through the video by clicking the time rail on an unbuffered portion of the video. In general, requesting just a chunk of a file from a Web server is called a range request or “byte serving.
真正的原因应该是服务器允许每次请求下载文件的一部分。这种被称为“progressive download” 或者 “range request”
If the Accept-Ranges is present in HTTP responses (and its value isn't "none"), the server supports range requests. You can check this by issuing a HEAD request and cURL, for example.In this response, Accept-Ranges: bytes indicates that bytes can be used as unit to define a range. Here the Content-Length header is also useful as it indicates the full size of the image to retrieve
curl -I http://i.imgur.com/z4d4kWk.jpg HTTP/1.1 200 OK ... Accept-Ranges: bytes Content-Length: 146515
"Accept-Ranges" 字段在返回头部出现,则说明服务器支持 range request ,这个字段的值是 bytes 。然后还需要的就是 "Content-Length" 标出文件字节长度。
其实,如果使用 webapi 的 StreamContent 可以返回整个视频文件,也可以实现播放,并且在浏览器端也是渐进式加载视频文件。
<video id="video" controls width="800" height="600">
<source src="/api/videoapi/PlayVideo/1" type="video/mp4"/>
</video>
[HttpGet] public HttpResponseMessage PlayVideo(string id) { try { string filePath = ("F:\\c#\\WebApplication1\\WebApplication1\\Upload\\24854-376704-huipkn44.wae_H264_1500kbps_AAC_und_ch2_96kbps.mp4"); HttpResponseMessage rsp = this.Request.CreateResponse(HttpStatusCode.OK); byte[] buffer = System.IO.File.ReadAllBytes(filePath); rsp.Content = new StreamContent(new MemoryStream(buffer)); rsp.Headers.Add("ContentType", "video/mp4"); return rsp; } catch (Exception e) { throw; } }
这样写可以播放视频,但是有个问题,不能拖动进度条。range request 可以解决进度条的问题。
引自:Easy Handling of Http Range Requests in ASP.NET
IIS 7 supports range requests natively, so if you are simply serving the video files directly from IIS you won’t have any problems. In this case, all file requests were being served by an ASPX page – it looked up information in a database as well as did some authorization checking before returning the file. Unfortunately range requests are not natively supported from aspx responses – they have to be handled manually
在上面的方法中,如果将网站host到本地iis后,查看浏览器的media请求,会发现video文件并非一次被下载到浏览器,可能就是因为 iis 10(win10)的自带功能,这个我并没有理解。
文章中的代码:
private void RangeDownload(string fullpath,HttpContext context)
{
long size,start,end,length,fp=0;
using (StreamReader reader = new StreamReader(fullpath))
{
size = reader.BaseStream.Length;
start = 0;
end = size - 1;
length = size;
// Now that we've gotten so far without errors we send the accept range header
/* At the moment we only support single ranges.
* Multiple ranges requires some more work to ensure it works correctly
* and comply with the spesifications: http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html#sec19.2
*
* Multirange support annouces itself with:
* header('Accept-Ranges: bytes');
*
* Multirange content must be sent with multipart/byteranges mediatype,
* (mediatype = mimetype)
* as well as a boundry header to indicate the various chunks of data.
*/
context.Response.AddHeader("Accept-Ranges", "0-" + size);
// header('Accept-Ranges: bytes');
// multipart/byteranges
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html#sec19.2
if (!String.IsNullOrEmpty(context.Request.ServerVariables["HTTP_RANGE"]))
{
long anotherStart = start;
long anotherEnd = end;
string[] arr_split = context.Request.ServerVariables["HTTP_RANGE"].Split(new char[] { Convert.ToChar("=") });
string range = arr_split[1];
// Make sure the client hasn't sent us a multibyte range
if (range.IndexOf(",") > -1)
{
// (?) Shoud this be issued here, or should the first
// range be used? Or should the header be ignored and
// we output the whole content?
context.Response.AddHeader("Content-Range", "bytes " + start + "-" + end + "/" + size);
throw new HttpException(416, "Requested Range Not Satisfiable");
}
// If the range starts with an '-' we start from the beginning
// If not, we forward the file pointer
// And make sure to get the end byte if spesified
if (range.StartsWith("-"))
{
// The n-number of the last bytes is requested
anotherStart = size - Convert.ToInt64(range.Substring(1));
}
else
{
arr_split = range.Split(new char[] { Convert.ToChar("-") });
anotherStart = Convert.ToInt64(arr_split[0]);
long temp = 0;
anotherEnd = (arr_split.Length > 1 && Int64.TryParse(arr_split[1].ToString(), out temp)) ? Convert.ToInt64(arr_split[1]) : size;
}
/* Check the range and make sure it's treated according to the specs.
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
*/
// End bytes can not be larger than $end.
anotherEnd = (anotherEnd > end) ? end : anotherEnd;
// Validate the requested range and return an error if it's not correct.
if (anotherStart > anotherEnd || anotherStart > size - 1 || anotherEnd >= size)
{
context.Response.AddHeader("Content-Range", "bytes " + start + "-" + end + "/" + size);
throw new HttpException(416, "Requested Range Not Satisfiable");
}
start = anotherStart;
end = anotherEnd;
length = end - start + 1; // Calculate new content length
fp = reader.BaseStream.Seek(start, SeekOrigin.Begin);
context.Response.StatusCode = 206;
}
}
// Notify the client the byte range we'll be outputting
context.Response.AddHeader("Content-Range", "bytes " + start + "-" + end + "/" + size);
context.Response.AddHeader("Content-Length", length.ToString());
// Start buffered download
context.Response.WriteFile(fullpath, fp, length);
context.Response.End();
}
这段代码是可以完全应用到项目中,其中参数 HttpContext context ,可以使用 HttpContext.Current 。
还有一点需要注意,既然是 rang request ,那么返回的 http status code 是 206 , 即代表 partial content 。
最后:使用 webapi 中的对象,同样思路实现了一版
[HttpGet]
public HttpResponseMessage PlayVideoRange(string id) {
try
{
string filePath = ("F:\\c#\\WebApplication1\\WebApplication1\\Upload\\24854-376704-huipkn44.wae_H264_1500kbps_AAC_und_ch2_96kbps.mp4");
HttpResponseMessage rsp = this.Request.CreateResponse(HttpStatusCode.PartialContent);
using (StreamReader sr = new StreamReader(filePath))
{
long size =0,start =0, end = 0;
size = sr.BaseStream.Length;
end = size - 1;
//rsp.Headers.AcceptRanges.Add("0-" + length.ToString());
rsp.Headers.AcceptRanges.Add("bytes");
if (this.Request.Headers.Range != null)
{
RangeItemHeaderValue requestRange = this.Request.Headers.Range.Ranges.First();
start = requestRange.From.Value;
long count = 1024 * 1024 * 5;
long anotherEnd = (start + count) > end ? end : (start + count);
end = requestRange.To == null ? anotherEnd : requestRange.To.Value;
}
sr.BaseStream.Seek(start, SeekOrigin.Begin);
byte[] buffer = new byte[end - start + 1];
sr.BaseStream.Read(buffer, 0, buffer.Length);
MemoryStream ms = new MemoryStream(buffer);
rsp.Content = new StreamContent(ms);
rsp.Content.Headers.ContentRange = new ContentRangeHeaderValue(start, end, size);
rsp.Content.Headers.ContentType = new MediaTypeHeaderValue("video/mp4");
return rsp;
}
}
catch (InvalidByteRangeException invalidByteRangeException)
{
return Request.CreateErrorResponse(invalidByteRangeException);
}
catch (Exception e) {
throw;
}
}
<video id="video" controls width="800" height="600">
<source src="/api/videoapi/PlayVideoRange/1" type="video/mp4" />
</video>