摄像头能力在视频会议、在线课堂、直播推流等Web场景中已经是刚需。Media Session API原本用于控制音频视频的播放元数据,而其扩展规范Media Session给出了setCameraActive这样的方法,允许页面通知浏览器当前摄像头处于激活还是关闭状态,浏览器侧据此更新系统UI或会话指示器。问题在于,这套API较新,TypeScript的lib.dom.d.ts还没有收录它,直接调用会报“属性不存在”的编译错误。本文就来解决类型定义与状态同步这两个核心问题。

一、为什么需要手动定义Set Camera Active的类型
先看一下原生API的调用形态。Media Session的摄像头激活方法挂载在navigator.mediaSession对象上,调用方式大致是navigator.mediaSession.setActionHandler(...)与一个专门的状态设置方法配合使用。在Chrome的实现中,方法签名为接收一个布尔值并返回Promise<void>,表示通知浏览器摄像头是否处于活跃状态。
由于TypeScript标准库尚未包含这个方法,编译器会认为navigator.mediaSession上不存在该属性。如果图省事用(navigator.mediaSession as any).setCameraActive(true)绕过去,就丢失了类型检查的意义:参数类型、返回值、异常处理全都处于裸奔状态。正确的做法是通过声明合并(declaration merging)扩展MediaSession接口,让编译器真正认识这个新能力。
还要注意一点,这个API运行在安全上下文(HTTPS或localhost)中,且需要页面已获得用户授权的媒体流。类型定义只能解决编译期问题,运行期的能力检测依然要做,这一点在后面的封装方案中会一并处理。
二、声明合并:为MediaSession接口补充类型
TypeScript的接口声明合并特性天生适合这类场景。我们在项目里新建一个media-session.d.ts文件,在其中重新声明MediaSession接口,编译器会自动把两处声明合并在一起:
/**
* 扩展 Media Session 规范中的摄像头激活状态方法
*/
interface MediaSession {
/**
* 通知浏览器当前摄像头是否处于激活状态
* @param active true 表示摄像头已开启,false 表示已关闭
*/
setCameraActive(active: boolean): Promise<void>;
}
interface Navigator {
mediaSession: MediaSession;
}这段声明做了两件事:一是给MediaSession接口补上setCameraActive方法;二是确保navigator.mediaSession的类型指向合并后的接口。由于声明合并是结构化的,即使将来官方类型库收录了这个方法,只要签名一致就不会冲突,收编成本几乎为零。
如果项目使用模块化组织代码,别忘了在文件顶部加一行export {};把它标记为模块,否则某些工程配置下全局声明可能无法被正确拾取。另外建议把激活状态建模成一个更语义化的类型,而不是裸的boolean:
/** 摄像头状态枚举:未激活 / 请求中 / 激活 / 错误 */
export enum CameraActiveState {
Inactive = 'inactive',
Pending = 'pending',
Active = 'active',
Error = 'error',
}
/** 与浏览器同步时使用的布尔载荷 */
export type CameraActivePayload = boolean;用枚举表达状态、用单独的类型别名表达同步载荷,两者各司其职。枚举方便在UI层做穷举渲染,布尔载荷则严格对齐浏览器API的入参要求,避免把内部状态直接透传给原生方法。
三、封装带能力检测的状态同步管理器
类型定义好之后,还需要一个运行期的同步层,负责能力检测、状态流转和异常兜底。直接在业务代码里到处调用setCameraActive既难测试也难维护。下面是一个经过实践检验的封装:
class CameraActiveSync {
private state: CameraActiveState = CameraActiveState.Inactive;
/** 判断当前环境是否支持 Media Session 摄像头状态同步 */
static isSupported(): boolean {
return (
typeof navigator !== 'undefined' &&
navigator.mediaSession !== undefined &&
typeof navigator.mediaSession.setCameraActive === 'function'
);
}
/** 将内部状态同步给浏览器,失败时静默降级并标记错误 */
async sync(payload: CameraActivePayload): Promise<CameraActiveState> {
if (!CameraActiveSync.isSupported()) {
// 浏览器不支持时直接返回当前状态,不阻塞业务
return this.state;
}
this.state = CameraActiveState.Pending;
try {
await navigator.mediaSession.setCameraActive(payload);
this.state = payload ? CameraActiveState.Active : CameraActiveState.Inactive;
} catch {
this.state = CameraActiveState.Error;
}
return this.state;
}
getState(): CameraActiveState {
return this.state;
}
}这个类的关键设计有三处。第一,isSupported用typeof判断方法是否为函数,这是最安全的鸭子检测方式,比检查版本号可靠得多。第二,sync方法内部维护了状态机,外部只能通过返回值感知结果,调用方拿到的永远是明确的CameraActiveState。第三,异常被捕获后转入Error态而不是直接抛出,因为摄像头状态同步属于增强能力,不应该因为它的失败把整个业务流程炸掉。
还有一个容易踩的坑:摄像头开关的时机。理想做法是在MediaStreamTrack的onended事件和track.enabled变化处触发同步,而不是让业务层手动调用。可以再加一个监听方法:
function bindTrackToSync(track: MediaStreamTrack, sync: CameraActiveSync): void {
track.addEventListener('ended', () => {
void sync.sync(false);
});
// 每秒轮询 enabled 状态,保证本地开关也能同步
const timer = setInterval(() => {
void sync.sync(track.enabled);
}, 1000);
// 返回清理函数,组件卸载时调用
// return () => clearInterval(timer);
}轮询虽然不如事件驱动优雅,但enabled属性的变化不会触发任何标准事件,这是目前比较务实的折中方案。如果对实时性要求高,可以把轮询间隔缩短到几百毫秒,代价是极小的CPU开销。
四、严格模式下的类型收窄与测试技巧
在strict: true的工程配置下,上面的isSupported判断其实还差一步。TypeScript并不知道类型守卫和运行时检查的关系,如果想让sync内部的navigator.mediaSession.setCameraActive通过严格空值检查,最好把能力检测写成自定义守卫:
function hasSetCameraActive(
session: MediaSession | undefined
): session is MediaSession & { setCameraActive(active: boolean): Promise<void> } {
return session !== undefined && typeof session.setCameraActive === 'function';
}这里的返回类型写法是类型谓词加交叉类型,语义是“当该函数返回true时,传入的对象可以被视为具备setCameraActive方法的MediaSession”。经过这个收窄,后续调用就不需要任何非空断言,代码完全处于类型系统的保护之下。
最后聊聊测试。浏览器环境可以通过Object.defineProperty在navigator.mediaSession上打桩模拟setCameraActive,用vi.fn()之类的mock函数验证同步行为;Node环境则干脆构造一个实现同名接口的假对象注入。得益于前面定义的枚举与类型守卫,测试用例可以直接按CameraActiveState的四个取值组织断言,覆盖度一目了然。类型定义先行,状态机收口,能力检测兜底,这三板斧下来,Set Camera Active在任何TypeScript项目里都能落地得干净利落。
TypeScriptMedia Session API摄像头激活状态修改时间:2026-09-07 00:54:42