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

Posted

技术标签:

【中文标题】以现有实际解决方案为例,正确处理视频流【英文标题】:Handling video streaming correctly on the example of existing real-world solutions 【发布时间】:2021-09-27 12:09:17 【问题描述】:

我正在试验/建立一个具有流媒体功能的网站,用户可以在其中上传视频 (.mp4) 并在页面上查看视频(视频内容部分/增量返回)。我正在使用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 中,我们可以看到正在下载的媒体文件。事实上,如果你使用 seek 功能 - 会出现多条请求记录:

我目前的问题是,在文件完全下载/播放到流结束后 - 如果您再次开始播放 - 浏览器 将再次开始下载内容,而不是已经使用缓存/下载的内容。

因此,我开始调查 YouTube 和 Instagram 等巨大的市场领导者如何处理视频流,但我注意到在视频播放时 - 没有出现单一的“媒体”类型的内容/请求。

事实上,从inspector 的角度来看 - 似乎根本没有下载任何内容。所以这引发了一些问题。

    YouTubeInstagram 等市场领导者如何向客户流式传输视频,而不暴露于 inspector 任何媒体流量? .Net 5 是否可以复制此行为? 为什么我的应用程序会一遍又一遍地重新下载同一个文件,如何防止这种情况发生? 为什么在使用DownloadStreamingAsync() 方法时搜索功能不起作用,如何解决?我使用这种方法主要是因为在这种情况下应用程序的内存占用更小。在将内容返回给客户端之前,我们不必将整个流下载到内存中。

虽然我在这里列出了 3 个问题 - 我主要关心的是第一个问题,因此非常欢迎任何关于该主题的答案。 :)

【问题讨论】:

【参考方案1】:

好的,经过一番研究和折腾 - 我找到了一篇关于这个主题的好文章:

How video streaming works on the web: An introduction

https://medium.com/canal-tech/how-video-streaming-works-on-the-web-an-introduction-7919739f7e1

简短的故事是 - 您可以编写一个将 MediaSource 注入标签的 javascript 代码,而不是将源文件直接提供给 &lt;video&gt; html 标签。然后,您可以使用Fetch APIXMLHttpRequest 之类的技术编写自定义逻辑,将视频流注入缓冲区。在XMLHttpRequest 类型的技术的情况下,您似乎会看到xhr 类型的请求,而在Fetch API - fetch 类型的请求中检查器.. 这个主题更加复杂和复杂,我对它没有广泛的了解。部分原因是我不是 JS 开发人员 :) 还因为您必须了解编解码器/编码器/视频数据帧的工作原理等。

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

Fetch API vs XMLHttpRequest

您可以研究的另一个值得注意的主题是流格式/标准。例如,您可以使用自适应流技术根据带宽容量调整视频质量。两个流媒体标准是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,
        ;
    

【讨论】:

> "为什么使用DownloadStreamingAsync()方法时seek功能不起作用以及如何解决?我使用这种方法主要是因为应用程序在这种情况下占用的内存较小。我们不在将内容返回给客户端之前,必须将整个流下载到内存中。”你找到这个问题的答案了吗?我也想知道使用带有 seek 的 DownloadStreamingAsync 的解决方案 @NateRadebaugh 是的,我找到了答案。检查编辑的帖子。【参考方案2】:

这是我必须要做的事情。最终,它使用 blob 并使用请求中的范围标头仅下载文件的一部分。

由于没有自动支持 Azure Blob 的 EnableRangeProcessing 的神奇 PhysicalFile-esc 帮助程序,我们手动完成。

浏览器查找要返回的 206: Partial Content 标头,连同 Accept-Ranges: bytesContent-LengthContent-Range 响应标头,以便知道是否还有更多视频要下载。

[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");

【讨论】:

以上是关于以现有实际解决方案为例,正确处理视频流的主要内容,如果未能解决你的问题,请参考以下文章

stm32 播放高帧率高分辨率视频和照片详细制作过程(播放Bad Apple为例)

以flourish为例,数据可视化基本操作|技巧分享

android实时视频网络传输方案总结(一共有五套)

android实时视频网络传输方案总结(一共有五套)

Visual Stadio 与NX二次开发的环境配置(以VS2010NX10.0为例)

mediaelement.js - 具有固定最大尺寸的响应式视频