导读:本期聚焦于半夏创作的《TypeScript中定义MediaRecorder Video Scaling Mode API视频缩放模式类型》,敬请观看详情。浏览器原生的MediaRecorder API并不存在直接名为Video Scaling Mode的独立接口,视频缩放模式实际上是通过HTMLVideoElement和CSS、以及canvas绘制过程中的坐标计算来控制的。在TypeScript项目里,如果希望对视频录制时的缩放模式进行强类型约束,可以自行定义ScalingMode类型的联合类型,再封装一个带类型检查的视频合成与录制工具函数。本文从分析需求场景入手,讲解如何用类型别名与枚举定义contain、cover、fill等缩放模式,配合canvas的drawImage实现等比缩放、裁剪与拉伸三种效果,再通过MediaRecorder把canvas流录制为WebM或MP4文件,并给出完整的类型定义与代码示例,帮助你构建类型安全、易于维护的视频录制模块。

在前端开发中处理视频录制需求时,经常遇到的一个问题是:摄像头采集的画面比例和目标录制比例不一致。比如摄像头输出的是16:9的1920x1080画面,而产品要求录制出1:1的正方形视频,或者需要固定输出640x480的4:3画面。这时候就必须在录制前对画面进行缩放处理,而浏览器并没有提供一个现成的Video Scaling Mode API,需要开发者借助canvas作为中间层,自己实现缩放逻辑。用TypeScript来做这件事的好处非常明显:缩放模式、参数对象、录制配置都可以被严格约束,避免拼错字符串导致的运行时错误。

TypeScript中定义MediaRecorder Video Scaling Mode API视频缩放模式类型

为什么需要自定义缩放模式类型

熟悉CSS的同学都知道,图片有object-fit属性,视频元素本身也支持。但MediaRecorder录制的不是DOM元素,而是媒体流。如果直接把getUserMedia得到的视频流交给MediaRecorder,录制出来的画面比例就是摄像头原始比例,无法改变。要控制输出比例,标准做法是把视频绘制到canvas上,在绘制阶段完成缩放,再把canvas的流交给MediaRecorder。

缩放逻辑本身有几种典型策略:等比缩放完整保留画面(可能出现黑边)、等比缩放裁掉超出部分(类似CSS的cover)、直接拉伸填满(会变形)。这三种策略在各个平台上命名不统一,Android有SCALE_TO_FIT这样的常量,iOS的AVPlayer有resize、resizeAspect、resizeAspectFill。在TypeScript中我们可以定义一套自己的类型系统,让这些模式有明确的类型身份,调用方只能从固定几个值里选择,写错了编译器会直接报错,这就比到处传魔法字符串可靠得多。

定义ScalingMode类型与配置接口

首先定义缩放模式的联合类型。用字符串字面量联合类型比枚举更轻量,而且调试时在控制台看到的值是可读的字符串。接着定义录制器的配置接口,把目标宽高、帧率、MIME类型都纳入类型约束。MIME类型可以用MediaRecorder.isTypeSupported做运行时校验,同时用类型系统保证传入的配置对象结构正确。

/** 视频缩放模式 */
type VideoScalingMode =
  | 'contain'   // 等比缩放,完整显示,留黑边
  | 'cover'     // 等比缩放,裁剪溢出部分,填满画布
  | 'fill';     // 直接拉伸,可能变形

/** 录制配置 */
interface ScaleRecordOptions {
  targetWidth: number;
  targetHeight: number;
  scalingMode: VideoScalingMode;
  frameRate?: number;
  mimeType?: string;
}

/** 默认配置 */
const DEFAULT_OPTIONS: Required<ScaleRecordOptions> = {
  targetWidth: 720,
  targetHeight: 720,
  scalingMode: 'cover',
  frameRate: 30,
  mimeType: 'video/webm;codecs=vp9',
};

这里有几个细节值得说明。Required<T>把所有可选属性变成必选,配合展开运算符可以方便地实现默认值合并。类型检查能保证scalingMode只接受三个值之一,假如调用方误写成'covers',TypeScript编译阶段就会提示类型不匹配,而不必等到测试时才发现画面没有按预期处理。

实现三种缩放算法并录制canvas流

缩放算法的核心是根据源视频的宽高和目标画布的宽高,计算绘制区域的位置和尺寸。三种模式对应三组计算公式:contain模式取宽高缩放比例中较小的一个,cover模式取较大的一个,fill模式直接铺满整个画布。计算完成后,用requestAnimationFrame循环绘制,并通过canvas.captureStream拿到可录制的媒体流。

function computeDrawRect(
  srcW: number, srcH: number, opts: Required<ScaleRecordOptions>
): { x: number; y: number; w: number; h: number } {
  const { targetWidth: tw, targetHeight: th, scalingMode } = opts;

  if (scalingMode === 'fill') {
    // 拉伸填满,不做等比处理
    return { x: 0, y: 0, w: tw, h: th };
  }

  const scaleW = tw / srcW;
  const scaleH = th / srcH;
  // contain取较小比例,cover取较大比例
  const ratio = scalingMode === 'contain'
    ? Math.min(scaleW, scaleH)
    : Math.max(scaleW, scaleH);

  const w = srcW * ratio;
  const h = srcH * ratio;
  // 居中放置,contain会产生黑边,cover会超出画布被裁掉
  return { x: (tw - w) / 2, y: (th - h) / 2, w, h };
}

async function startScaledRecording(
  stream: MediaStream, options: Partial<ScaleRecordOptions> = {}
): Promise<MediaRecorder> {
  const opts = { ...DEFAULT_OPTIONS, ...options };

  const video = document.createElement('video');
  video.srcObject = stream;
  video.muted = true;
  await video.play();

  const canvas = document.createElement('canvas');
  canvas.width = opts.targetWidth;
  canvas.height = opts.targetHeight;
  const ctx = canvas.getContext('2d')!;

  // 黑边模式先填充黑色背景
  if (opts.scalingMode === 'contain') {
    ctx.fillStyle = '#000';
  }

  const render = (): void => {
    const rect = computeDrawRect(
      video.videoWidth, video.videoHeight, opts
    );
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    ctx.drawImage(video, rect.x, rect.y, rect.w, rect.h);
    requestAnimationFrame(render);
  };
  requestAnimationFrame(render);

  const canvasStream = canvas.captureStream(opts.frameRate);
  const recorder = new MediaRecorder(canvasStream, {
    mimeType: opts.mimeType,
  });
  recorder.start(1000);
  return recorder;
}

调用端的代码非常简洁,先通过getUserMedia拿到摄像头流,然后指定目标尺寸和缩放模式即可开始录制。停止录制时调用recorder.stop(),把ondataavailable事件收集到的Blob合并成文件。由于函数签名中options是Partial<ScaleRecordOptions>类型,调用方可以只传想覆盖的字段,其余自动使用默认值,这就是类型定义带来的使用体验提升。

async function demo(): Promise<void> {
  const camera = await navigator.mediaDevices.getUserMedia({
    video: { width: 1920, height: 1080 },
    audio: true,
  });

  const recorder = await startScaledRecording(camera, {
    targetWidth: 720,
    targetHeight: 720,
    scalingMode: 'contain',
  });

  const chunks: Blob[] = [];
  recorder.ondataavailable = (e: BlobEvent): void => {
    if (e.data.size > 0) chunks.push(e.data);
  };
  recorder.onstop = (): void => {
    const blob = new Blob(chunks, { type: recorder.mimeType });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = 'recorded.webm';
    a.click();
    URL.revokeObjectURL(url);
  };

  // 录制10秒后自动停止
  setTimeout((): void => recorder.stop(), 10000);
}

有几个容易踩的坑需要注意。第一,video.play()必须等待完成再开始绘制,否则videoWidth还是0,画面会一直是黑的;第二,contain模式记得在每帧绘制前用fillRect清空画布并填充背景色,否则上一帧残留的影像会叠加;第三,MIME类型在不同浏览器支持情况不同,Safari对WebM支持不完整,建议通过MediaRecorder.isTypeSupported做特性检测,回退到video/mp4。把这些处理封装进类型化的工具函数后,整个录制模块就可以在各种比例需求的场景下安全复用了。

TypeScriptMediaRecorder视频缩放模式修改时间:2026-09-16 20:24:46

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0916/58168.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。