在浏览器端做视频录制时,MediaRecorder构造函数的第二个参数里有一个videoBitsPerSecond字段,它决定了输出视频的码率高低,直接影响画质和文件体积。很多开发者只是随手传一个数字,比如2500000就完事了。但在实际项目中,码率往往需要根据分辨率、帧率甚至网络状况动态计算,这时候就需要设计一套合理的TypeScript类型来描述码率算法。本文就来完整梳理这套类型该怎么定义。

一、先搞清楚MediaRecorder的原生类型定义
在lib.dom.d.ts中,MediaRecorder的构造函数签名大致是这样的:new MediaRecorder(stream: MediaStream, options?: MediaRecorderOptions)。其中MediaRecorderOptions只包含三个可选字段:mimeType、audioBitsPerSecond和videoBitsPerSecond,后两者都是number类型,单位是比特每秒。
原生类型只接受一个静态数字,这意味着如果想要动态码率,TypeScript层面不会给你任何约束和提示,所有逻辑都得自己在外面拼装。理解这一点很重要:我们要做的不是改写浏览器API,而是在它之上构建一层类型安全的封装。
另外要注意,videoBitsPerSecond传0在部分浏览器里表示让浏览器自行选择码率,这个行为并没有在所有实现中统一,所以封装时最好把这个特殊情况显式建模,避免使用者误传。
二、用联合类型建模三种码率策略
码率策略本质上是一个可扩展的集合,用类型别名加判别联合来建模是最合适的。我们可以定义恒定码率、动态码率、自动码率三种形态,每一种都用一个字面量字段做判别。
// 判别联合:描述视频码率的三种策略
export type VideoBitrateStrategy =
| { mode: 'constant'; bitrate: number } // CBR 恒定码率
| { mode: 'auto' } // 交给浏览器自行决定
| { mode: 'dynamic'; compute: BitrateComputer }; // 动态计算
// 码率计算函数:输入采集信息,输出比特每秒
export type BitrateComputer = (info: CaptureInfo) => number;
export interface CaptureInfo {
width: number;
height: number;
frameRate: number;
hasAudio: boolean;
}这样定义的好处是,使用者传入{ mode: 'constant', bitrate: 4000000 }时,TypeScript会强制要求带上bitrate字段;而传{ mode: 'auto' }时如果还带了多余的bitrate字段,配合exactOptionalPropertyTypes或显式校验就能发现问题。判别联合的核心价值就是把非法状态从类型层面排除掉,比如不可能出现既没有bitrate又没有compute函数的残缺配置。
如果你的TS版本较新,还可以用satisfies运算符让字面量对象在保留推断精度的同时接受检查,进一步提升配置代码的可读性和安全性。
三、实现分辨率驱动的动态码率算法
动态码率最常见的形式是根据分辨率计算。业界常用的经验公式是按像素总量乘以一个系数,再结合帧率微调。例如1080p大约800万像素每秒的带宽需求,可以推导出每像素比特数的基准值。
// 基于分辨率与帧率的码率估算函数
export const pixelBasedBitrate: BitrateComputer = ({ width, height, frameRate }) => {
const pixels = width * height;
// 每像素基准码率:0.1 比特,帧率越高系数略微上浮
const bitsPerPixel = 0.1 * (frameRate / 30);
return Math.round(pixels * frameRate * bitsPerPixel);
};
// 使用示例:720p 30fps 约 2.7 Mbps
const strategy: VideoBitrateStrategy = {
mode: 'dynamic',
compute: pixelBasedBitrate,
};这个函数在720p 30fps下算出的结果大约是276万比特每秒,处于WebM编码的合理区间。需要注意的是,不同编码格式对码率的敏感度不一样,VP9和H.264在相同画质下码率差异可达百分之三十以上,所以更严谨的做法是把mimeType也纳入CaptureInfo,让计算函数可以根据编码格式调整系数。
另一个实践建议是给计算结果设置上下限。分辨率极低时算出的码率可能低于音频码率导致画质崩坏,分辨率极高时又可能超出设备编码能力,所以用Math.min和Math.max做夹逼是必要的防御手段。
四、把策略转换为MediaRecorderOptions的解析层
类型定义完成后,还需要一个纯函数把策略解析成浏览器认识的原生参数。这一层是封装的出口,也是类型发挥价值的地方:函数签名明确声明输入是策略、输出是合法的MediaRecorderOptions。
export function resolveBitrateOptions(
strategy: VideoBitrateStrategy,
info: CaptureInfo
): Pick<MediaRecorderOptions, 'videoBitsPerSecond'> {
switch (strategy.mode) {
case 'constant':
return { videoBitsPerSecond: strategy.bitrate };
case 'dynamic':
return { videoBitsPerSecond: clampBitrate(strategy.compute(info)) };
case 'auto':
return {}; // 不传字段,由浏览器决定
}
}
// 夹逼到合理区间,防止极端值
function clampBitrate(value: number): number {
const MIN = 500_000; // 下限 500 kbps
const MAX = 20_000_000; // 上限 20 Mbps
return Math.min(Math.max(value, MIN), MAX);
}注意switch中不需要default分支,因为strategy.mode已经被判别联合约束为三种字面量之一,穷尽性检查会确保未来新增策略时编译器立刻报错提醒补全逻辑。这是判别联合配合switch的典型优势,也是很多大型前端项目钟爱这种模式的原因。
最后在业务侧的使用就变得非常清爽:先根据video元素的实际尺寸组装CaptureInfo,再调用解析函数拿到配置,最后传给MediaRecorder构造函数。整个过程每一步都有类型保障,码率算法被替换或扩展时,调用方代码几乎不用改动,只需要新增一个判别分支。这种封装思路同样适用于音频码率、录制格式选择等其他MediaRecorder配置项,值得作为通用的录制模块设计范式沉淀下来。
TypeScriptMediaRecorder API视频码率修改时间:2026-09-09 20:46:48