以现有现实世界解决方案为例正确处理视频流
Handling video streaming correctly on the example of existing real-world solutions
我是 experimenting/building 一个具有 流媒体功能 的网站,用户可以在其中上传视频 (.mp4
) 并在页面上观看视频 (返回视频内容 partially/incrementally)。我正在使用 Azure Blob Storage
和 dotnet
技术堆栈。
html:
<video src="https://localhost:5001/Files/DownloadFileStream?fileName=VID_20210719_110859.mp4" controls="true" />
控制器:
[HttpGet]
public async Task<IActionResult> DownloadFileStream([FromQuery] string fileName)
{
var range = Request.Headers["range"].ToString();
Console.WriteLine(range);
var container = new BlobContainerClient("UseDevelopmentStorage=true", "sample-container");
await container.CreateIfNotExistsAsync();
BlobClient blobClient = container.GetBlobClient(fileName);
var response = await blobClient.GetPropertiesAsync();
string contentType = response.Value.ContentType;
long contentLength = response.Value.ContentLength;
Azure.Response<BlobDownloadStreamingResult> result = await blobClient.DownloadStreamingAsync();
return new FileStreamResult(result.Value.Content, contentType)
{
EnableRangeProcessing = true,
};
// following approach supports video Seek functionality:
/*
using var memory = new MemoryStream();
await blobClient.DownloadToAsync(memory);
return new FileContentResult(memory.ToArray(), contentType)
{
EnableRangeProcessing = true,
};*/
}
我注意到,当我流式传输内容时 - 在 Chrome 的 Inspect mode
中,我们可以看到正在下载的媒体文件。事实上,如果您使用搜索功能 - 将出现多个请求记录:
我目前的问题是,文件完全下载后 downloaded/played 直到流结束 - 如果您再次开始播放 - 浏览器 将再次开始下载内容 ,而不是使用已经 cached/downloaded 的内容。
所以我去调查了 YouTube 和 Instagram 等巨大的市场领导者如何处理视频流,但我注意到在视频播放时 - 没有出现 content/request 的单一“媒体”类型。
事实上,从 inspector
的角度来看 - 似乎根本没有下载任何内容。所以这引发了一些问题。
- YouTube 和 Instagram 等市场领导者如何在不暴露给
inspector
任何媒体流量的情况下向客户流式传输视频? .Net 5
是否可以复制此行为?
- 为什么我的应用程序不断重复下载同一个文件,如何防止这种情况发生?
- 为什么在使用
DownloadStreamingAsync()
方法时搜索功能不起作用,如何解决?我之所以使用这种方法,主要是因为在这种情况下应用程序的内存占用较小。在将内容返回给客户端之前,我们不必将整个流下载到内存中。
虽然我在这里列出了 3 个问题 - 我主要关心的是第一个问题,因此非常欢迎任何有关该主题的答案。 :)
好的,经过一番学习和磨练后 - 我找到了一个很好的 article/s 主题:
How video streaming works on the web: An introduction
https://medium.com/canal-tech/how-video-streaming-works-on-the-web-an-introduction-7919739f7e1
简而言之 - 不是直接向 <video>
html
标签提供源文件 - 您可以编写一个 javascript 代码,将 MediaSource 注入标签.然后,您可以使用 Fetch API
或 XMLHttpRequest
之类的技术编写自定义逻辑,将视频流注入缓冲区。如果是 XMLHttpRequest
类型的技术,您似乎会在检查器中看到 xhr
类型的请求,如果是 Fetch API
- fetch
类型的请求。主题复杂得多,我对此没有广泛的了解。部分原因是我不是 JS 开发人员 :) 但也因为你必须了解 codecs/encoders/video 数据帧的工作原理等等。
关于上述技术的更多细节:
您可以研究的另一个值得注意的主题是流式传输 formats/standards。例如,您可以使用自适应流技术根据带宽容量调整视频质量。两个流媒体标准是 HLS
(较旧的)和 DASH
(较新的?)。
此外,既然我们正在做 - 我建议研究高级视频播放器库:
- video.js: https://github.com/videojs/video.js
- hls.js: https://github.com/video-dev/hls.js/
- dash.js: https://github.com/Dash-Industry-Forum/dash.js/
如果您在 github 上查找它们 - 您将对整个主题有所了解。
编辑 11.08.2021
关于我自己关于 DownloadStreamingAsync
方法用法的问题和回答@Nate 的问题 - 你不能使用这种方法。 Download
的意思是 - 给我全部内容。你正在寻找的是 OpenReadAsync
方法,它可以让你获得流。
这是工作示例(我自己不会使用它,但有人可能会觉得它有用):
[HttpGet]
[Route("Stream4/{fileName}")]
public async Task<IActionResult> Get3(
[FromRoute] string fileName,
CancellationToken ct)
{
var container = new BlobContainerClient("UseDevelopmentStorage=true", "sample-container");
await container.CreateIfNotExistsAsync();
BlobClient blobClient = container.GetBlobClient(fileName);
var response = await blobClient.GetPropertiesAsync();
string contentType = response.Value.ContentType;
long contentLength = response.Value.ContentLength;
long kBytesToReadAtOnce = 300;
long bytesToReadAtOnce = kBytesToReadAtOnce * 1024;
var result = await blobClient.OpenReadAsync(0, bufferSize: (int)bytesToReadAtOnce, cancellationToken: ct);
return new FileStreamResult(result, contentType)
{
// this is important
EnableRangeProcessing = true,
};
}
这是我要处理的事情。最终它获取 blob 并使用请求中的范围 headers 仅下载文件的一部分。
由于没有神奇的 PhysicalFile
-esc 帮助程序自动支持 Azure Blob EnableRangeProcessing
,我们全部手动完成。
浏览器查找要返回的 206: Partial Content
header,连同 Accept-Ranges: bytes
、Content-Length
和 Content-Range
响应 headers 所以它知道是否有更多的视频要下载。
[HttpGet, Route("video")]
public async Task<IActionResult> VideoAsync()
{
var rangeHeaders = Request.GetTypedHeaders().Range;
var blobServiceClient = new BlobServiceClient(connectionString);
var containerClient = blobServiceClient.GetBlobContainerClient("data");
var blobClient = containerClient.GetBlobClient(fileName);
if (blobClient.Exists())
{
var getPropertiesResponse = await blobClient.GetPropertiesAsync();
string contentType = getPropertiesResponse.Value.ContentType;
long contentLength = getPropertiesResponse.Value.ContentLength;
Azure.HttpRange? range = null;
if (rangeHeaders?.Ranges?.Count > 0)
{
var rangeHeader = rangeHeaders.Ranges.First();
// 1. If the unit is not 'bytes'.
// 2. If there are multiple ranges in header value.
// 3. If start or end position is greater than file length.
if (rangeHeaders.Unit != "bytes" || rangeHeaders.Ranges.Count > 1)
{
Response.StatusCode = (int)HttpStatusCode.RequestedRangeNotSatisfiable;
Response.Headers.Add("Content-Range", $"bytes {range.Value.Offset}-{contentLength - 1}/{contentLength}");
Response.Headers.Add("Content-Type", "video/mp4");
return null;
}
var offset = rangeHeader.From ?? 0;
var length = rangeHeader.From.HasValue && rangeHeader.To.HasValue
? rangeHeader.To - rangeHeader.From
: null;
range = new Azure.HttpRange(offset, length);
}
// Append appropriate response headers based on status
Response.Headers.Add("Accept-Ranges", "bytes");
Response.Headers.Add("Content-Length", $"{contentLength}");
if (range.HasValue)
{
Response.Headers.Add("Content-Range", $"bytes {range.Value.Offset}-{range.Value.Length ?? (contentLength - 1)}/{contentLength}");
}
var streamingFileResponse = await blobClient.DownloadStreamingAsync(range ?? default);
var videoContent = streamingFileResponse.Value.Content;
var videoMeta = streamingFileResponse.Value.Details;
var fileResponse = new FileStreamResult(videoContent, "video/mp4");
Response.StatusCode = (int)HttpStatusCode.PartialContent;
return fileResponse;
}
throw new BadHttpRequestException("Bad request");
}
我是 experimenting/building 一个具有 流媒体功能 的网站,用户可以在其中上传视频 (.mp4
) 并在页面上观看视频 (返回视频内容 partially/incrementally)。我正在使用 Azure Blob Storage
和 dotnet
技术堆栈。
html:
<video src="https://localhost:5001/Files/DownloadFileStream?fileName=VID_20210719_110859.mp4" controls="true" />
控制器:
[HttpGet]
public async Task<IActionResult> DownloadFileStream([FromQuery] string fileName)
{
var range = Request.Headers["range"].ToString();
Console.WriteLine(range);
var container = new BlobContainerClient("UseDevelopmentStorage=true", "sample-container");
await container.CreateIfNotExistsAsync();
BlobClient blobClient = container.GetBlobClient(fileName);
var response = await blobClient.GetPropertiesAsync();
string contentType = response.Value.ContentType;
long contentLength = response.Value.ContentLength;
Azure.Response<BlobDownloadStreamingResult> result = await blobClient.DownloadStreamingAsync();
return new FileStreamResult(result.Value.Content, contentType)
{
EnableRangeProcessing = true,
};
// following approach supports video Seek functionality:
/*
using var memory = new MemoryStream();
await blobClient.DownloadToAsync(memory);
return new FileContentResult(memory.ToArray(), contentType)
{
EnableRangeProcessing = true,
};*/
}
我注意到,当我流式传输内容时 - 在 Chrome 的 Inspect mode
中,我们可以看到正在下载的媒体文件。事实上,如果您使用搜索功能 - 将出现多个请求记录:
我目前的问题是,文件完全下载后 downloaded/played 直到流结束 - 如果您再次开始播放 - 浏览器 将再次开始下载内容 ,而不是使用已经 cached/downloaded 的内容。
所以我去调查了 YouTube 和 Instagram 等巨大的市场领导者如何处理视频流,但我注意到在视频播放时 - 没有出现 content/request 的单一“媒体”类型。
事实上,从 inspector
的角度来看 - 似乎根本没有下载任何内容。所以这引发了一些问题。
- YouTube 和 Instagram 等市场领导者如何在不暴露给
inspector
任何媒体流量的情况下向客户流式传输视频?.Net 5
是否可以复制此行为? - 为什么我的应用程序不断重复下载同一个文件,如何防止这种情况发生?
- 为什么在使用
DownloadStreamingAsync()
方法时搜索功能不起作用,如何解决?我之所以使用这种方法,主要是因为在这种情况下应用程序的内存占用较小。在将内容返回给客户端之前,我们不必将整个流下载到内存中。
虽然我在这里列出了 3 个问题 - 我主要关心的是第一个问题,因此非常欢迎任何有关该主题的答案。 :)
好的,经过一番学习和磨练后 - 我找到了一个很好的 article/s 主题:
How video streaming works on the web: An introduction
https://medium.com/canal-tech/how-video-streaming-works-on-the-web-an-introduction-7919739f7e1
简而言之 - 不是直接向 <video>
html
标签提供源文件 - 您可以编写一个 javascript 代码,将 MediaSource 注入标签.然后,您可以使用 Fetch API
或 XMLHttpRequest
之类的技术编写自定义逻辑,将视频流注入缓冲区。如果是 XMLHttpRequest
类型的技术,您似乎会在检查器中看到 xhr
类型的请求,如果是 Fetch API
- fetch
类型的请求。主题复杂得多,我对此没有广泛的了解。部分原因是我不是 JS 开发人员 :) 但也因为你必须了解 codecs/encoders/video 数据帧的工作原理等等。
关于上述技术的更多细节:
您可以研究的另一个值得注意的主题是流式传输 formats/standards。例如,您可以使用自适应流技术根据带宽容量调整视频质量。两个流媒体标准是 HLS
(较旧的)和 DASH
(较新的?)。
此外,既然我们正在做 - 我建议研究高级视频播放器库:
- video.js: https://github.com/videojs/video.js
- hls.js: https://github.com/video-dev/hls.js/
- dash.js: https://github.com/Dash-Industry-Forum/dash.js/
如果您在 github 上查找它们 - 您将对整个主题有所了解。
编辑 11.08.2021
关于我自己关于 DownloadStreamingAsync
方法用法的问题和回答@Nate 的问题 - 你不能使用这种方法。 Download
的意思是 - 给我全部内容。你正在寻找的是 OpenReadAsync
方法,它可以让你获得流。
这是工作示例(我自己不会使用它,但有人可能会觉得它有用):
[HttpGet]
[Route("Stream4/{fileName}")]
public async Task<IActionResult> Get3(
[FromRoute] string fileName,
CancellationToken ct)
{
var container = new BlobContainerClient("UseDevelopmentStorage=true", "sample-container");
await container.CreateIfNotExistsAsync();
BlobClient blobClient = container.GetBlobClient(fileName);
var response = await blobClient.GetPropertiesAsync();
string contentType = response.Value.ContentType;
long contentLength = response.Value.ContentLength;
long kBytesToReadAtOnce = 300;
long bytesToReadAtOnce = kBytesToReadAtOnce * 1024;
var result = await blobClient.OpenReadAsync(0, bufferSize: (int)bytesToReadAtOnce, cancellationToken: ct);
return new FileStreamResult(result, contentType)
{
// this is important
EnableRangeProcessing = true,
};
}
这是我要处理的事情。最终它获取 blob 并使用请求中的范围 headers 仅下载文件的一部分。
由于没有神奇的 PhysicalFile
-esc 帮助程序自动支持 Azure Blob EnableRangeProcessing
,我们全部手动完成。
浏览器查找要返回的 206: Partial Content
header,连同 Accept-Ranges: bytes
、Content-Length
和 Content-Range
响应 headers 所以它知道是否有更多的视频要下载。
[HttpGet, Route("video")]
public async Task<IActionResult> VideoAsync()
{
var rangeHeaders = Request.GetTypedHeaders().Range;
var blobServiceClient = new BlobServiceClient(connectionString);
var containerClient = blobServiceClient.GetBlobContainerClient("data");
var blobClient = containerClient.GetBlobClient(fileName);
if (blobClient.Exists())
{
var getPropertiesResponse = await blobClient.GetPropertiesAsync();
string contentType = getPropertiesResponse.Value.ContentType;
long contentLength = getPropertiesResponse.Value.ContentLength;
Azure.HttpRange? range = null;
if (rangeHeaders?.Ranges?.Count > 0)
{
var rangeHeader = rangeHeaders.Ranges.First();
// 1. If the unit is not 'bytes'.
// 2. If there are multiple ranges in header value.
// 3. If start or end position is greater than file length.
if (rangeHeaders.Unit != "bytes" || rangeHeaders.Ranges.Count > 1)
{
Response.StatusCode = (int)HttpStatusCode.RequestedRangeNotSatisfiable;
Response.Headers.Add("Content-Range", $"bytes {range.Value.Offset}-{contentLength - 1}/{contentLength}");
Response.Headers.Add("Content-Type", "video/mp4");
return null;
}
var offset = rangeHeader.From ?? 0;
var length = rangeHeader.From.HasValue && rangeHeader.To.HasValue
? rangeHeader.To - rangeHeader.From
: null;
range = new Azure.HttpRange(offset, length);
}
// Append appropriate response headers based on status
Response.Headers.Add("Accept-Ranges", "bytes");
Response.Headers.Add("Content-Length", $"{contentLength}");
if (range.HasValue)
{
Response.Headers.Add("Content-Range", $"bytes {range.Value.Offset}-{range.Value.Length ?? (contentLength - 1)}/{contentLength}");
}
var streamingFileResponse = await blobClient.DownloadStreamingAsync(range ?? default);
var videoContent = streamingFileResponse.Value.Content;
var videoMeta = streamingFileResponse.Value.Details;
var fileResponse = new FileStreamResult(videoContent, "video/mp4");
Response.StatusCode = (int)HttpStatusCode.PartialContent;
return fileResponse;
}
throw new BadHttpRequestException("Bad request");
}