使用.net后端流式传输大视频文件以响应前端

问题描述 投票:0回答:1

当我创建一个前端为 React 、后端为 .NET Core 7.0 的电子学习 Web 应用程序时,我偶然发现了一个对我来说很困难的主题。假设我有授权用户想要观看1小时长的视频,重量很大(比方说30GB) - 在反应中我可能会使用这样的东西:

const [videoUrl, setVideoUrl] = useState<string>();
axiosPrivate
        .get(`/SomeVideoService/Video?id=${id}`, { responseType: "blob" })
        .then((response) => {
          const videoBlobUrl = URL.createObjectURL(response.data);
          setVideoUrl(videoBlobUrl);
        })
        .catch((error) => {
          console.error("Error fetching video:", error);
        });
return (
    <div>
      <video width="640" height="360" controls>
        <source src={videoUrl} type="video/mp4" />
        Your browser does not support the video tag.
      </video>
    </div>
  );

服务是这样写的:

    [HttpGet("Video")]
    [Authorize(Roles = "FirstRole, SecondRole, ThirdRole")]
    public async Task<IActionResult> GetVideo(string id)
    {
        var video = "video.mp4";
        var filePath = Path.Combine(Directory.GetCurrentDirectory().ToString(), "Videos", video);

        if (!System.IO.File.Exists(filePath))
        {
            return NotFound();
        }
        var memory = new MemoryStream();
        using (var stream = new FileStream(filePath, FileMode.Open))
        {
            stream.CopyTo(memory);
        }
        memory.Position = 0;

        return File(memory, "video/mp4", Path.GetFileName(filePath));
    }

但是我不确定这一切的作用 - 我看到了结果并且它正确地显示在我的屏幕上(我没有 1 小时长的视频,只有 1 分钟)。在网络选项卡中,我看到多个数据:图像/请求,状态代码附近写入 200 OK(来自内存缓存)。问题如下:

  • 当用户想要观看仅供授权用户观看的视频时,该视频是否应该由后端视频服务作为内存流发送,就像我上面所做的那样?静态文件是没有选择的,因为用户必须获得授权。
  • 前端服务器到底是如何工作的? “外部”服务器上的一个实例,用户可以使用浏览器看到它?我的意思是前端包含逻辑和视图,您需要浏览器(具有您的 IP 并使用协议来联系前端服务器)来查看所有内容,并让服务器在浏览器中为用户生成其实例的临时版本(或将其视图投射到用户上)浏览器)? [这个问题不太重要,因为答案可能是一些很酷的教程/解释,因为我找不到任何对我有用的东西]
  • (1) 提供的代码的流程是这样的:用户单击“显示视频” - >浏览器将其发送到前端服务器 - >前端服务器向后端应用提取并获取完整视频 - >浏览器从前端获取视频 blob 文件分块服务器;现在我看到数据:图像/请求
  • (2) 或者提供的代码的流程是这样的(我认为这种方式更正确):用户点击“显示视频” - >浏览器直接发送到后端服务获取方法来获取视频 - >视频被完整下载到用户的浏览器(如果文件很重,则大不不),然后浏览器将其分成更小的块并从 blob 对象接收它们 (主要区别在于用户调用后端 api 后视频存储的位置 -> 用户浏览器内存还是前端服务器内存?)
  • 如果第二个选项是答案 - 那么我应该使用 Ranged 标头作为后端获取端点吗?
    [HttpGet("Video")]
    [Authorize(Roles = "Common, Extended")]
    public async Task<IActionResult> GetVideo(string id)
    {
        var video = "video.mp4";
        var filePath = Path.Combine(Directory.GetCurrentDirectory(), "Videos", video);
        if (!System.IO.File.Exists(filePath))
        {
            return NotFound();
        }

        var fileInfo = new FileInfo(filePath);
        long fileLength = fileInfo.Length;
        var lastModified = fileInfo.LastWriteTimeUtc;
        var entityTag = new EntityTagHeaderValue($"\"{fileInfo.LastWriteTimeUtc.Ticks}\"");

        var requestHeaders = Request.GetTypedHeaders();
        var responseHeaders = Response.GetTypedHeaders();
        responseHeaders.CacheControl = new CacheControlHeaderValue
        {
            Public = true,
            MaxAge = TimeSpan.FromSeconds(3600)
        };
        responseHeaders.LastModified = lastModified;
        responseHeaders.ETag = entityTag;

        if (requestHeaders.IfModifiedSince.HasValue && requestHeaders.IfModifiedSince.Value >= lastModified)
        {
            return StatusCode(StatusCodes.Status304NotModified);
        }

        if (requestHeaders.IfNoneMatch != null && requestHeaders.IfNoneMatch.Contains(entityTag))
        {
            return StatusCode(StatusCodes.Status304NotModified);
        }

        var range = requestHeaders.Range;
        if (range == null || !RangeHelper.TryGetRangeItem(range, fileLength, out var rangeItem))
        {
            return File(System.IO.File.OpenRead(filePath), "video/mp4", enableRangeProcessing: false);
        }

        var start = rangeItem.From ?? 0;
        var end = rangeItem.To ?? (fileLength - 1);

        var contentLength = end - start + 1;
        var contentRange = new ContentRangeHeaderValue(start, end, fileLength);

        Response.StatusCode = StatusCodes.Status206PartialContent;
        responseHeaders.ContentRange = contentRange;

        var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
        fileStream.Seek(start, SeekOrigin.Begin);

        return File(fileStream, "video/mp4", enableRangeProcessing: true, fileDownloadName: null, lastModified, entityTag, contentLength);
    }

或者,如果我想执行此方法,那么我应该将 1 小时长的视频剪切成更小的(10 秒)文件,并通过此端点为它们提供服务,并以某种方式在前端执行此操作(相同的反应片段将用于不剪切)版本):

import React, { useState, useEffect } from 'react';

const VideoPlayer = ({ videoId }) => {
  const [videoUrl, setVideoUrl] = useState('');

  useEffect(() => {
    const apiUrl = `SomeVideoService/Video${videoId}`;
    setVideoUrl(apiUrl);
  }, [videoId]);

  if (!videoId) {
    return <div>Loading...</div>;
  }

  return (
    <div>
      <video width="640" height="360" controls>
        <source src={videoUrl} type="video/mp4" />
        Your browser does not support the video tag.
      </video>
    </div>
  );
};

export default VideoPlayer;

抱歉内容太长,希望有人有足够的耐心帮我写下来。感谢您阅读所有内容并与本文互动:)

reactjs .net video streaming
1个回答
0
投票

带有

MemoryStream
的第一个代码示例对于您的用例来说非常糟糕,因为它在提供服务之前将整个视频加载到服务器上的内存中。即使对于单个请求,这也将是巨大的资源量,更不用说多个请求了。 需要支持 HTTP Range。使用 HTTP 范围,您不必担心将视频文件分成块,因为 Web 浏览器会自动请求它想要的文件部分,并且服务器将仅使用该范围内的文件中的字节进行适当响应。当用户浏览视频或“寻找”视频时,浏览器将自动发送必要的 HTTP 范围标头。

但是,我认为在采用这种方法之前您应该暂停一下。这样做只是一种美化的(但效率惊人的)下载协议。 1 小时 30GB 相当大,许多用户没有足够的带宽来观看它。

这就是像

HLS

这样的协议存在的原因。巧合的是,HLS 是建立在您之前提到的“块”概念之上的。但是有很多库不仅可以对视频进行编码,还可以对播放器等进行编码。许多浏览器和平台已经内置支持它,尤其是最近(几年前我处理视频时这将是天赐之物!)。 HLS 更为复杂,只有当您想要获得好处时才应该走这条路。它需要以多个比特率对视频进行编码/分块。但这就是好处;质量将根据用户的可用带宽无缝变化。如果您有大量内容并且想要在各种网络连接质量下工作,这可能是必要的。

对于授权,是的,这只是您在后端服务中处理的事情。如果他们未经授权,那么您只需回复 401 即可。

您关于“前端服务器”的问题有点令人困惑。除非您不遗余力地拥有前端微服务,否则您通常只会从与其他服务器相同的服务器为前端提供服务。

除非您指的是前端开发服务器,例如 Webpack 和 Vite 等提供的服务器。但您不会在生产中运行这些服务器。

© www.soinside.com 2019 - 2024. All rights reserved.