You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何修改FileStreamResult响应的Content-Length?视频分段加载故障

问题:通过Google Drive API实现视频流式传输与Range请求处理

我正在构建一个媒体网关Web API,通过Google Drive API获取图片和视频返回给前端。图片功能已实现,但视频返回存在两个核心问题:

  • 未处理Range请求时,需完整下载260MB视频,耗时过长
  • 尝试处理Range请求后,仅返回视频前4秒内容,响应状态码为200而非预期的206,且Content-Length被错误设置为分块大小而非实际文件大小

现有问题代码1:完整下载视频(播放时可能重复下载)

[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;
}

现有问题代码2:尝试处理Range请求但仅返回部分内容

[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;
}

问题分析

Chrome开发者工具显示响应头异常:

Accept-Ranges: bytes
Content-Length: 10485761
Content-Type: video/mp4
Date: Wed, 24 Jan 2024 15:57:52 GMT
Server: Kestrel

响应状态码为200而非206:

Request URL: https://localhost:7026/file/video
Request Method: GET
Status Code: 200 OK
Remote Address: [::1]:7026
Referrer Policy: strict-origin-when-cross-origin

推测原因是FileStreamResult自动将Content-Length设置为当前流的长度(分块大小),而非视频实际总大小,导致浏览器无法正确识别分块范围,进而无法正常流式播放。

解决方案

要正确处理Range请求并返回206状态码,需要手动构建响应头,而非依赖FileStreamResult的自动处理。具体步骤如下:

  1. 解析客户端Range请求:正确提取起始和结束字节位置,处理多种Range格式(如bytes=0-1024、bytes=1024-等)
  2. 获取Google Drive文件总大小:提前调用API获取文件实际大小
  3. 构建响应头:设置Content-Range、Content-Length、Accept-Ranges等关键头
  4. 返回分块流:直接写入响应流,避免使用FileStreamResult覆盖头信息

修正后的代码

[HttpGet("video")]
public async Task<IActionResult> GetVideo()
{
    string credentialsPath = "Api-key-for-google-drive.json";
    string fileId = "{fileId}";

    // 初始化Google Drive服务
    using var credentialsFileStream = new FileStream(credentialsPath, FileMode.Open, FileAccess.Read);
    var scopes = new[] { DriveService.ScopeConstants.DriveFile };
    var googleCredential = GoogleCredential.FromStream(credentialsFileStream).CreateScoped(scopes);
    var driveService = new DriveService(new BaseClientService.Initializer
    {
        HttpClientInitializer = googleCredential,
        ApplicationName = "Media Gateway API"
    });

    // 获取文件总大小
    var fileRequest = driveService.Files.Get(fileId);
    fileRequest.Fields = "size";
    var googleFile = await fileRequest.ExecuteAsync();
    var totalFileSize = googleFile.Size ?? throw new Exception("无法获取文件大小");

    // 解析Range请求
    RangeHeaderValue? rangeHeader = Request.Headers.Range;
    long start = 0;
    long end = totalFileSize - 1;

    if (rangeHeader != null && rangeHeader.Ranges.Any())
    {
        var range = rangeHeader.Ranges.First();
        start = range.From ?? 0;
        end = range.To ?? (totalFileSize - 1);

        // 修正超出范围的请求
        if (start >= totalFileSize)
        {
            return StatusCode(StatusCodes.Status416RequestedRangeNotSatisfiable, 
                new { error = "请求范围超出文件大小" });
        }
        end = Math.Min(end, totalFileSize - 1);
    }

    // 计算当前分块大小
    long chunkSize = end - start + 1;

    // 设置响应头
    Response.StatusCode = StatusCodes.Status206PartialContent;
    Response.Headers.AcceptRanges = "bytes";
    Response.Headers.ContentRange = $"bytes {start}-{end}/{totalFileSize}";
    Response.ContentLength = chunkSize;
    Response.ContentType = "video/mp4";

    // 从Google Drive下载指定范围并写入响应流
    var downloadRequest = driveService.Files.Get(fileId);
    await downloadRequest.DownloadRangeAsync(Response.Body, new RangeHeaderValue(start, end));
    
    return new EmptyResult();
}

关键改进点

  • 直接操作响应流:跳过FileStreamResult,避免其自动覆盖Content-Length等头信息
  • 正确解析Range请求:处理多种合法的Range格式,并修正超出范围的请求
  • 手动设置响应头:明确指定Content-Range、Content-Length和状态码206,让浏览器正确识别分块内容
  • 流式传输:直接将Google Drive的分块数据写入响应流,无需先存入内存,降低内存占用

内容的提问来源于stack exchange,提问作者Cornelis

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 14:47:33