画中画(Picture in Picture)是桌面和移动浏览器都广泛支持的视频特性,用户可以把视频窗口悬浮在页面之外继续观看。为了让页面UI与画中画状态保持同步,W3C在Media Session API中提供了setPictureInPictureActive这个动作处理器,通过setActionHandler注册后,浏览器进入或退出画中画时就会回调它。问题在于,部分TypeScript内置的lib.dom.d.ts版本并没有包含这个API的类型声明,直接调用会提示属性不存在。这篇文章就来解决这个问题:如何自己写出一份既准确又具备兼容性的类型定义。

一、先理清Media Session API的类型脉络
Media Session API的核心对象是navigator.mediaSession,它的类型是MediaSession。在这个接口上,metadata属性接受一个MediaMetadata对象,用于设置标题、艺术家、专辑和封面;而setActionHandler方法则负责注册各种动作的回调,比如播放、暂停、上一曲、下一曲,以及与画中画相关的enterpictureinpicture、leavepictureinpicture等动作。
在新版浏览器规范中,画中画状态同步不再要求你分别监听进入和退出两个动作,而是提供了一个独立的方法setPictureInPictureActive(isActive: boolean)。浏览器会主动调用它,并传入一个布尔值表示当前是否处于画中画模式。理解这一点很重要:它是浏览器到页面的单向通知,而不是页面主动调用的方法。因此类型定义上,它是一个挂载在MediaSession原型上的普通方法即可,但由于旧版TS声明文件没有收录,我们需要用声明合并(declaration merging)的方式补充它。
先看一段不写类型声明时直接使用的报错代码:
// 旧版TS会报错:Property 'setPictureInPictureActive' does not exist on type 'MediaSession'
navigator.mediaSession.setPictureInPictureActive((isActive: boolean) => {
console.log('画中画状态:', isActive);
});这段代码在运行时是完全符合规范的,Chrome等浏览器可以正常执行,但编译阶段会被TypeScript拦截。这正是需要我们补充类型声明的根本原因。
二、编写完整的类型声明文件
解决思路是在项目中添加一个.d.ts文件,通过interface的声明合并特性给MediaSession接口追加方法。新建一个media-session.d.ts,内容如下:
// media-session.d.ts
interface MediaSession {
/**
* 注册画中画状态同步回调
* 浏览器在进入或退出画中画时调用,isActive 表示当前是否处于画中画模式
*/
setPictureInPictureActive(callback: (isActive: boolean) => void): void;
}
interface Window {
// 部分浏览器将方法暴露在 window 上,做兼容声明
setPictureInPictureActive?(callback: (isActive: boolean) => void): void;
}这里有两个细节值得展开。第一,声明合并要求接口名与全局已有的接口完全一致,所以直接写interface MediaSession而不需要import任何东西,前提是这个d.ts文件不包含顶层的import或export语句,否则会变成模块文件而失去合并能力。第二,Window上的声明加了可选修饰符,用?标出,这样在没有暴露该方法的浏览器环境中访问时,TS能正确提示需要做空值判断。
如果你的项目使用了较新的TypeScript版本,可以先检查内置声明是否已经包含这个方法。检查方式是按住Ctrl并点击mediaSession跳转到定义,搜索setPictureInPictureActive。如果已经存在,就不要重复声明,否则会出现签名冲突。稳妥的做法是把声明写在同一个接口名下,TypeScript对同名方法的不同签名会尝试合并重载,但签名不一致时可能产生意外行为,建议保持与官方一致的参数形式。
三、实战:在组件中实现画中画状态同步
类型补齐之后,就可以放心地在业务代码中使用它了。下面是一个完整的前端示例,展示如何让页面按钮的显示状态与视频的实际画中画状态保持同步:
// pip-sync.ts
export function setupPipSync(
video: HTMLVideoElement,
onStateChange: (isActive: boolean) => void
): void {
if (!('mediaSession' in navigator)) {
console.warn('当前浏览器不支持 Media Session API');
return;
}
// 注册画中画状态回调
navigator.mediaSession.setPictureInPictureActive((isActive) => {
// isActive 由浏览器传入,true 表示已进入画中画
onStateChange(isActive);
});
// 双保险:同时监听 video 元素自身的进入/离开事件
video.addEventListener('enterpictureinpicture', () => onStateChange(true));
video.addEventListener('leavepictureinpicture', () => onStateChange(false));
}为什么不只依赖Media Session这一条路?因为在实际项目中,浏览器兼容性参差不齐,Safari对Media Session的支持范围与Chrome不同,而enterpictureinpicture和leavepictureinpicture这两个事件在video元素上是更早标准化的能力。两条通道同时监听,再用一个防抖或状态去重逻辑兜底,可以保证按钮状态不会出现抖动或滞后。
在React等框架中使用时,建议把状态同步逻辑放在useEffect中,并在清理函数里移除事件监听,避免组件卸载后回调仍然引用已销毁的状态。示例结构如下:
import { useEffect, useState } from 'react';
function usePipActive(videoRef: React.RefObject<HTMLVideoElement>) {
const [isActive, setIsActive] = useState(false);
useEffect(() => {
const video = videoRef.current;
if (!video) return;
const cleanup = setupPipSync(video, setIsActive);
return () => {
cleanup?.();
};
}, [videoRef]);
return isActive;
}这样组件内部拿到的isActive就始终是可信的画中画状态,无论是用户通过浏览器的画中画按钮触发,还是通过Media Session的通知中心操作,UI都能第一时间响应。
四、声明合并的注意事项与调试技巧
补充类型声明时有几个常见的坑。首先是文件位置:d.ts必须被tsconfig.json的include字段覆盖到,通常放在src/types目录下并在配置中包含该目录即可。如果发现声明不生效,最快的排查方法是在使用处故意写错参数类型,看TS是否报出你定义的签名错误;如果提示的还是属性不存在,说明文件根本没被编译进来。
其次要注意与第三方库声明的冲突。有些音视频相关的npm包自带了对Media Session的扩展声明,如果你又手动声明了一遍,可能出现重复标识符错误。此时可以把自己的声明改成条件式的写法,或者直接依赖第三方库的类型。最后,建议在声明中写清楚JSDoc注释,这样团队其他成员在IDE中悬停查看时能立刻明白回调参数的含义和调用时机,减少沟通成本。类型定义不仅是编译通过的门票,更是一份活的接口文档。
TypeScriptMedia Session API画中画修改时间:2026-09-09 01:32:48