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

使用Nest.js+@aws-sdk/client-s3从S3流视频遇问题求助

Nest.js + AWS S3 视频流式传输修复:解决进度无法拖动、总时长延迟显示问题

问题根源

当前代码的核心问题是将整个视频文件一次性加载到内存后再返回给客户端,完全忽略了浏览器播放视频时依赖的HTTP范围请求(Range Requests)机制。视频的进度拖动、提前显示总时长这两个功能,都需要服务器支持接收Range请求,并返回对应的视频片段及正确的响应头。

修复代码

import { Controller, Get, Query, Req, Res, NotFoundException, HttpStatus } from '@nestjs/common';
import { StreamableFile } from '@nestjs/common/file-stream';
import { GetObjectCommand, HeadObjectCommand, S3Client } from '@aws-sdk/client-s3';
import { Request, Response } from 'express';

@Controller('video')
export class VideoController {
  constructor(private readonly client: S3Client) {}

  async getVideoFromStorage(
    @Query('path') pathToStorage: string,
    @Req() req: Request,
    @Res({ passthrough: true }) res: Response,
  ) {
    if (!pathToStorage) {
      throw new NotFoundException('请提供文件路径');
    }

    // 预获取文件元数据,拿到总大小并验证文件存在
    try {
      const headResponse = await this.client.send(
        new HeadObjectCommand({
          Bucket: 'bucket',
          Key: pathToStorage,
        }),
      );
      const fileSize = headResponse.ContentLength;
      if (!fileSize) throw new Error('文件大小获取失败');

      // 处理Range请求头
      const range = req.headers.range;
      if (range) {
        // 解析Range格式:bytes=start-end
        const parts = range.replace(/bytes=/, '').split('-');
        const start = parseInt(parts[0], 10);
        const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1;

        // 校验Range合法性
        if (start >= fileSize || end >= fileSize || start > end) {
          res.status(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE);
          res.setHeader('Content-Range', `bytes */${fileSize}`);
          return;
        }

        const chunkSize = end - start + 1;
        // 请求S3指定范围的视频片段
        const objectResponse = await this.client.send(
          new GetObjectCommand({
            Bucket: 'bucket',
            Key: pathToStorage,
            Range: `bytes=${start}-${end}`,
          }),
        );

        // 设置片段响应头
        res.status(HttpStatus.PARTIAL_CONTENT);
        res.setHeader('Accept-Ranges', 'bytes');
        res.setHeader('Content-Range', `bytes ${start}-${end}/${fileSize}`);
        res.setHeader('Content-Length', chunkSize.toString());
        res.setHeader('Content-Type', 'video/webm');

        return new StreamableFile(objectResponse.Body as NodeJS.ReadableStream);
      } else {
        // 无Range请求,返回完整文件(流式传输)
        const objectResponse = await this.client.send(
          new GetObjectCommand({
            Bucket: 'bucket',
            Key: pathToStorage,
          }),
        );

        res.setHeader('Accept-Ranges', 'bytes');
        res.setHeader('Content-Length', fileSize.toString());
        res.setHeader('Content-Type', 'video/webm');

        return new StreamableFile(objectResponse.Body as NodeJS.ReadableStream);
      }
    } catch (err) {
      throw new NotFoundException('文件不存在');
    }
  }
}

关键修复点说明

  1. 支持Range请求:解析浏览器发送的Range头,向S3请求对应范围的视频片段,实现进度拖动功能。
  2. 正确设置响应头:
    • Accept-Ranges: bytes:告知浏览器服务器支持范围请求
    • Content-Range:返回当前片段范围和文件总大小,让浏览器提前获取视频时长
    • Content-Length:针对请求片段返回对应大小,而非整个文件大小
  3. 流式传输优化:直接返回S3返回的可读流,避免将整个视频加载到内存,降低服务器资源占用
  4. HeadObject预校验:通过HeadObjectCommand提前获取文件元数据,既验证文件存在,又避免下载整个文件获取大小

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.06 23:05:13