
卷四收官篇。presentation 子系统core/src/dom/presentation/处理「播放器的呈现形态」——全屏、画中画、远程播放、屏幕方向锁。这是浏览器兼容的重灾区iOS Safari 的 webkit 前缀、Safari PWA 的 PiP 陷阱、方向锁的异步竞态。这篇看 v10 怎么把这些坑一个个填平。架构纯函数库 feature 装配先说一个我读源码时的发现presentation/下的文件fullscreen.ts、pip.ts等是纯函数库没有任何事件监听。事件监听在 store feature 层store/features/fullscreen.ts等。这个分离很清晰presentation/*.ts——无状态的浏览器兼容封装「怎么请求全屏」「怎么判断在不在全屏」。store/features/*.ts——feature 装配「什么时候同步状态到 store」「绑什么事件」。另外presentation/index.ts3 行只导出 fullscreen/pip/remote-playback——orientation 不在桶文件里仅由 orientation-lock feature 直接路径导入因为它不是「能力」而是「配置型 feature」。fullscreen三级降级 四层判定请求全屏的三级降级presentation/fullscreen.ts:54-79// L57-66container 存在且 document 支持全屏el.requestFullscreen()// L60-62 — 标准el.webkitRequestFullscreen()// L64-66 — webkit 前缀// L69-72都不行 →video.webkitSetPresentationMode(fullscreen)// iOS presentationMode 兜底// L75-78最后 →video.requestFullscreen()// 媒体自定义如 Cast 场景的 host 提供标准 → webkit 前缀 → iOS presentationMode → 媒体自定义。四级路径覆盖所有浏览器。退出全屏的顺序刻意相反exitFullscreenL81-102webkit 的 presentationMode 先退L84-88再标准doc.exitFullscreen()L90-92。为什么因为 iOS 的 presentation mode 是独立状态得先解除它标准 API 才能生效。判断「在不在全屏」的四层判定isFullscreenL29-52// 1. L30-33 — iOS presentationModevideo.webkitPresentationModefullscreen// 2. L35-38 — 标准/webkit 的 fullscreenElement 比对fullscreenElementcontainer||fullscreenElementmedia// 3. L44-46 — :fullscreen 伪类matchesFullscreen 内部 try/catchL20-27container.matches(:fullscreen)||media.matches(:fullscreen)// 4. L50-51 — 非标准 video.isFullscreen由 video host 设置第 3 层的注释L40-43解释了为什么需要它:fullscreen伪类匹配全屏元素及其祖先跨 shadow boundary——覆盖「内层video通过原生控件进全屏」的情况此时 fullscreenElement 是内层元素但 container 是它的祖先。第 4 层的video.isFullscreen是非标准属性——由 media host15 篇设置覆盖 Cast 这种「概念上的全屏」接收端全屏本地 document 无感知。能力探测的动态探针isFullscreenEnabledL5-13doc.fullscreenEnabled || doc.webkitFullscreenEnabled都假时动态创建一个video探针L11检测isFunction(video.webkitSetPresentationMode)L12——iOS Safari 的旧路径。这和 21 篇 volume 的canSetVolume是同一套「运行时探针」手法。feature 装配store/features/fullscreen.ts:44-66attach({target,signal,set}){set({fullscreenAvailability:isFullscreenEnabled()?available:unsupported});// L47-49constsync()set({fullscreen:isFullscreen(container,media)});// L51-54sync();// L56listen(document,fullscreenchange,sync,{signal});// L58listen(document,webkitfullscreenchange,sync,{signal});// L59if(webkitPresentationModeinvideo){// L62-65 — iOS 补充listen(media,webkitpresentationmodechanged,sync,{signal});}}同时绑标准和非标准事件iOS 再补一个。还有一个贴心细节L16-19requestFullscreen先退 PiP——注释写着 “browser behavior is inconsistent”有些浏览器全屏时会打断 PiP主动退更可控。pipSafari PWA 陷阱presentation/pip.ts的请求/退出逻辑和 fullscreen 对称webkit 优先/回退最有意思的是能力探测L5-14exportfunctionisPictureInPictureEnabled():boolean{if(document.pictureInPictureEnabled){// Safari PWA 排除UA 匹配 Safari 且 display-mode: standalone 时禁用if(/.*Version\/.*Safari\/.*/.test(navigator.userAgent)matchMedia((display-mode: standalone)).matches){returnfalse;// L7-9}returntrue;}// webkit 探针回退 L12-13}Safari 的 PWAstandalone 模式里 PiP 会崩——pictureInPictureEnabled说支持实际用就炸。v10 用 UA display-mode: standalone双重判定提前排除。这种坑不可能从规范推出来只能是实踩后写进代码的。feature 装配store/features/pip.ts:49-71监听enterpictureinpicture/leavepictureinpictureL63-64 iOS 的webkitpresentationmodechangedL67-70。togglePictureInPicture同样先退 fullscreenL34-46——PiP 和全屏互斥。remote-playback鸭子类型 浏览器 UIpresentation/remote-playback.ts27 行最短// L4-10 — 鸭子类型检测exportfunctionresolveRemote(media){if(isObject(media.remote)stateinmedia.remotepromptinmedia.remote){returnmedia.remote;// cast 成 MediaRemotePlaybackCapability[remote]}returnnull;}// L20-26 — 请求远程播放exportfunctionrequestRemotePlayback(media){constremoteresolveRemote(media);if(!remote)thrownewDOMException(Remote playback not supported,NotSupportedError);// L23remote.prompt();// L25 — AirPlay/Cast 的设备选择 UI 由浏览器弹出}注意 L25——remote.prompt()触发的是浏览器原生的设备选择 UIAirPlay/Cast 菜单。v10 不自己画设备列表。17 篇讲的GoogleCast组件提供了RemotePlayback的规范实现google-cast/remote-playback.ts这里消费的正是那个接口。orientation异步竞态的优雅解法presentation/orientation.ts77 行是我觉得这份源码里「小而美」的典范。问题screen.orientation.lock()是async的——await 期间用户可能已经调了unlock()。怎么防止「迟到的 lock 覆盖已生效的 unlock」解法是两个标志分离「事实」和「意图」L30-31letlockedfalse;// 事实lock 真正成功letdesiredfalse;// 意图调用方想要锁lock()的完整流程L45-65asynclock(){desiredtrue;// 先记意图// ... lock 非函数直接 returnL52不支持时静默try{awaitorientation.lock(type);// L55 — 异步等真正锁上if(desired)lockedtrue;// L58 — await 期间意图还在才置事实elsereleaseOrientation();// L60-64 — 意图已变立刻释放}catch{/* L56-58 */}}unlock(){desiredfalse;// L67 — 只改意图if(locked){releaseOrientation();lockedfalse;}// L68-70}L58 的if (desired)是竞态的裁决点await 回来时如果desired已经是 falseunlock 抢先了立刻调releaseOrientation()反向补偿——迟到的 lock 被立即撤销。这个「意图标志 完成后校验」的模式比 Promise 链或状态机都轻。orientation-lock featureconfigurable 实战store/features/orientation-lock.ts53 行是 19 篇讲的configurable feature的内置使用者// L16-52 — 可配置 feature默认 { type: landscape }L51orientationLockFeature({type:portrait})// 这样定制它的 attachL21-49做边沿检测wasFullscreen标志L25、L35记住上一次全屏态进入全屏lock()L29-30、退出unlock()L31-32——方向锁跟随全屏状态自动开关全屏看视频锁横屏退出解锁。同样监听三种 fullscreenchangeL40-46abort 兜底 unlockL48。小结presentation 子系统是「浏览器兼容教科书」带走这些手法纯函数库 feature 装配分离——兼容封装无状态监听在 feature。多级降级链——fullscreen 四级请求路径退出顺序刻意相反。四层全屏判定——presentationMode/fullscreenElement/:fullscreen伪类/非标准属性覆盖 shadow DOM 和 Cast。运行时探针——动态video探测 iOS 能力和 volume 的 canSetVolume 同款。Safari PWA 陷阱——UA display-mode 双判。意图/事实双标志解异步竞态——orientation 的 desired/locked。互斥主动处理——全屏先退 PiP、PiP 先退全屏“browser behavior is inconsistent”。至此卷四讲完。播放器核心的全貌feature 组装19-21、无头 Core22-23、选择器24、输入桥25、presentation26。下一篇进入卷五——SPF 流处理框架全系列的重头戏15 篇。