我想构建一个媒体网关 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
推荐人政策:跨源时严格源
我认为我已经非常接近我想要实现的目标了。我最好的猜测是内容长度必须更改为正确的长度,但我不知道如何更改。我想知道是否可以更改内容长度,如果可以,我该怎么做。
经过更多调查,我发现调用 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、文件名、上次修改时间。但我相信这段代码对于问题中描述的用法来说是一个很好的开始。