如何修改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的自动处理。具体步骤如下:
- 解析客户端Range请求:正确提取起始和结束字节位置,处理多种Range格式(如
bytes=0-1024、bytes=1024-等) - 获取Google Drive文件总大小:提前调用API获取文件实际大小
- 构建响应头:设置
Content-Range、Content-Length、Accept-Ranges等关键头 - 返回分块流:直接写入响应流,避免使用
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
相关产品推荐
相关产品推荐

