如何更改文件流结果响应的内容长度

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

我想构建一个媒体网关 Web API,它通过 API 从 google 驱动器检索图像和视频,并将其作为对前端 Web 应用程序的响应返回。在网关中,我想检查是否允许用户检索媒体。我的第一步是让 Web API 提供媒体成为可能。

我成功地让它适用于图像。对于视频来说,它也可以工作,但不是我想要的方式。我无法使其与范围标头值一起使用。如果没有它,就意味着整个视频(大小为260 MB)都被下载了,感觉需要很长时间。这应该可以通过流式传输视频来防止,但我还没有成功地实现这一点。

这是我有效的代码,尽管视频在播放时可能会处于加载状态,这会导致再次从谷歌驱动器完全下载视频。

[HttpGet("video")]
public async Task<IActionResult> GetVideo()
{
    string credentialsPath = "Api-key-for-google-drive.json";
    string folderId = "{folderId}";
   
    using var credentialsFileStream = new FileStream(credentialsPath, FileMode.Open, 
    FileAccess.Read);
    var scopes = new[] { DriveService.ScopeConstants.DriveFile };
    var googleCredential = GoogleCredential.FromStream(credentialsFileStream).CreateScoped(scopes);
    var baseClientService = new BaseClientService.Initializer
    {
        HttpClientInitializer = googleCredential,
        ApplicationName = "A name"
    };
    var driveService = new DriveService(baseClientService);
    var parents = new List<string> { folderId };

    var downloadRequest = driveService.Files.Get("{fileId}");
    var memoryStream = new MemoryStream();
    await downloadRequest.DownloadAsync(memoryStream);
    memoryStream.Position = 0;
    var result = new FileStreamResult(memoryStream, "video/mp4")
    {
        EnableRangeProcessing = true
    };

    return result;
}

根据文档,Google Drive API 也支持范围标头值,这部分似乎可以工作。但现在,视频只播放前 4 秒,然后就停止了。这是仅返回前几秒的代码:

[HttpGet("video")]
public async Task<IActionResult> GetVideo()
{
    var chunkSize = 1024 * 1024 * 10;

    var requestRange = Request.Headers.Range;

    //TODO implement retrieving the range better in the future
    var start = requestRange.Count > 0
        ? long.Parse(requestRange.First()!.Replace("bytes=", "").Replace("-", ""))
        : 0;

    string credentialsPath = "Api-key-for-google-drive.json";
    string folderId = "{fileId}";

    using var credentialsFileStream = new FileStream(credentialsPath, FileMode.Open, FileAccess.Read);
    var scopes = new[] { DriveService.ScopeConstants.DriveFile };
    var googleCredential = GoogleCredential.FromStream(credentialsFileStream).CreateScoped(scopes);
    var baseClientService = new BaseClientService.Initializer
    {
        HttpClientInitializer = googleCredential,
        ApplicationName = "Google Drive Upload Console App"
    };
    var driveService = new DriveService(baseClientService);
    var parents = new List<string> { folderId };

    var downloadRequest = driveService.Files.Get("{fileId}");
    downloadRequest.Fields = "size";
    var googleFile = await downloadRequest.ExecuteAsync();
    var size = googleFile.Size ?? -1;

    var end = start + chunkSize <= size ? start + chunkSize : size;

    RangeHeaderValue rangeHeaderValue = new(start, end);
    var memoryStream = new MemoryStream();

    await downloadRequest.DownloadRangeAsync(memoryStream, rangeHeaderValue);
    memoryStream.Position = 0;
    Response.ContentLength = size; //<-- makes no different, ContentLength has not the correct length
    var result = new FileStreamResult(memoryStream, "video/mp4")
    {
        EnableRangeProcessing = true
    };

    return result;
}

我最好的猜测是 FileStreamResult 仅通过内存流接收视频的第一部分。这会导致响应将 Content-Length 设置为流中内容的长度,而不是实际长度。我在chrome的开发工具的响应头中可以看到是这样的:

接受范围:字节
内容长度:10485761
内容类型:视频/mp4
日期:2024 年 1 月 24 日星期三 15:57:52 GMT
服务器:红隼

我认为这也会导致将响应状态代码设置为 200,而我预期为 206。根据文档,

EnableRangeProcessing = true
应将状态代码设置为 206。

请求网址:https://localhost:7026/file/video
请求方式:GET
状态代码:200 正常
远程地址:[::1]:7026
推荐人政策:跨源时严格源

我认为我已经非常接近我想要实现的目标了。我最好的猜测是内容长度必须更改为正确的长度,但我不知道如何更改。我想知道是否可以更改内容长度,如果可以,我该怎么做。

c# .net-core video google-drive-api asp.net-core-webapi
1个回答
0
投票

经过更多调查,我发现调用 File 或创建 FileStreamResult 实例仅限于我想要的内容。我通过检查源代码弄清楚了这一点。幸运的是,源代码也为我找到了问题的解决方案,这也为我提供了很多帮助,即创建我自己的 FileContentResult 版本:

public class PartialFileContentResult(MemoryStream partialFileStream, string contentType, long fileLength, long from, long to) : ActionResult
{
    private const string AcceptRangeHeaderValue = "bytes";

    private readonly MemoryStream partialFileStream = partialFileStream;
    private readonly string contentType = contentType;
    private readonly long fileLength = fileLength;
    private readonly long from = from;
    private readonly long to = to < fileLength ? to : fileLength - 1;

    private readonly long rangeLength = to - from + 1;

    public override Task ExecuteResultAsync(ActionContext context)
    {
        ArgumentNullException.ThrowIfNull(context);
        var response = context.HttpContext.Response;
        response.ContentType = contentType;
        response.ContentLength = rangeLength;
        response.StatusCode = StatusCodes.Status206PartialContent;
        response.Headers.AcceptRanges = AcceptRangeHeaderValue;

        var httpResponseHeaders = response.GetTypedHeaders();
        httpResponseHeaders.ContentRange = new ContentRangeHeaderValue(from, to, fileLength);

        using (partialFileStream)
        {
            try
            {
                partialFileStream.Seek(0, SeekOrigin.Begin);
                return StreamCopyOperation.CopyToAsync(partialFileStream, context.HttpContext.Response.Body, rangeLength, (int)partialFileStream.Length, context.HttpContext.RequestAborted);
            }
            catch (OperationCanceledException)
            {
                context.HttpContext.Abort();
                return Task.CompletedTask;
            }
        }
    }
}

这就是这样称呼的:

new PartialFileContentResult(memoryStream, "video/mp4", size, start, end);

而不是

new FileStreamResult(memoryStream, "video/mp4")
{
    EnableRangeProcessing = true
};

PartialFileContentResult 类实现目前非常基础,缺少很多实现,例如 Etag、文件名、上次修改时间。但我相信这段代码对于问题中描述的用法来说是一个很好的开始。

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