以现有现实世界解决方案为例正确处理视频流

Handling video streaming correctly on the example of existing real-world solutions

我是 experimenting/building 一个具有 流媒体功能 的网站,用户可以在其中上传视频 (.mp4) 并在页面上观看视频 (返回视频内容 partially/incrementally)。我正在使用 Azure Blob Storagedotnet 技术堆栈。

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 的角度来看 - 似乎根本没有下载任何内容。所以这引发了一些问题。

  1. YouTubeInstagram 等市场领导者如何在不暴露给 inspector 任何媒体流量的情况下向客户流式传输视频? .Net 5 是否可以复制此行为?
  2. 为什么我的应用程序不断重复下载同一个文件,如何防止这种情况发生?
  3. 为什么在使用 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 APIXMLHttpRequest 之类的技术编写自定义逻辑,将视频流注入缓冲区。如果是 XMLHttpRequest 类型的技术,您似乎会在检查器中看到 xhr 类型的请求,如果是 Fetch API - fetch 类型的请求。主题复杂得多,我对此没有广泛的了解。部分原因是我不是 JS 开发人员 :) 但也因为你必须了解 codecs/encoders/video 数据帧的工作原理等等。

关于上述技术的更多细节:

您可以研究的另一个值得注意的主题是流式传输 formats/standards。例如,您可以使用自适应流技术根据带宽容量调整视频质量。两个流媒体标准是 HLS(较旧的)和 DASH(较新的?)。

此外,既然我们正在做 - 我建议研究高级视频播放器库:

如果您在 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: bytesContent-LengthContent-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");
}