Maui Blazor 在 macOS 上 video 元素无法全屏的修复方法

问题

在 .NET MAUI Blazor 混合应用中嵌入 标签后,macOS 用户点击视频自带的全屏按钮,视频只会撑满整个 BlazorWebView 控件的区域。窗口本体并不进入 macOS 的独立桌面空间(Space),菜单栏和 Dock 栏仍然可见。这种“半全屏”体验和预期中的系统全屏差异明显。

检查配置,WKWebViewElementFullscreenEnabled 已经显式置为 true

e.Configuration.Preferences.ElementFullscreenEnabled = true;

完全没有效果。Apple 开发者论坛上也有不少讨论,确认 elementFullscreenEnabled 在 Mac Catalyst 环境下不起作用。

原理

一开始的思路很直接:网页里的视频要进入 macOS 原生的全屏空间,最合理的做法就是在 JS 侧捕捉全屏事件,通过消息通道传给原生层,再由原生层去操作真正的系统窗口。这条“JS → Native → 窗口操作”的通信链路,就是后面要反复用到的桥接。

方向定了,第一步就是去接标准全屏事件,结果直接撞上第一堵墙。document.addEventListener("fullscreenchange", ()=>{}); 无效。

Mac Catalyst 的 UIKit 底子

Mac Catalyst 的设计初衷是把 iPad 应用快速带到 Mac 上,所以底层依赖的是 UIKit,而不是 AppKit。顶层窗口在逻辑上是 UIWindow,负责网页渲染的 WebView 也来自 iOS 版本的 WKWebView,而不是桌面版 Safari 那一套。

HTML5 标准全屏模型——Element.requestFullscreen、``fullscreenchange` 事件——属于桌面 WebKit 的能力,需要与 AppKit 窗口服务器交互。iOS 上没有独立 Space 的概念,因此 iOS 版 WebKit 从最初就没有实现这套接口。基于 UIKit 的 Mac Catalyst 自然也继承了这一限制,标准全屏通路到这里就走不下去了。

前缀 API 同样无效

尝试带厂商前缀的版本 webkitfullscreenchange 事件,在桌面 Safari 中都可用,但放到 Mac Catalyst 里同样没有任何反应。这些方法在 iOS 版 WebKit 中直接落入空操作分支,调用不报错,行为上等于什么都没做。

仅存的信号:两个 iOS 残留事件

标准全屏走不通,但 元素点击全屏按钮后,视频还是会铺满 WKWebView 的可视区域。这说明底层必然有某种事件被触发。在 stackoverflow 上找到了相应的事件:

  • webkitbeginfullscreen
  • webkitendfullscreen

这两个事件和标准化的 webkitfullscreenchange 是两码事。它们是 iOS 上专门为移动端视频播放器内联全屏行为设计的遗留事件,分别对应进入和退出元素内全屏的时机。Mac Catalyst 在继承 iOS WebKit 时把它们一并带了过来,而且测试确认,在点击全屏按钮时这两个事件的确在正常触发。这就是仅有的、能够捕获用户全屏意图的信号。

桥接的逻辑

监听有了,桥接方案剩下的部分就清楚了:

  1. JS → Native 信号传递:用 webkitbeginfullscreenwebkitendfullscreen 捕获全屏状态变化,通过 WKScriptMessageHandler 把布尔值传到原生层。
  2. UIKit → AppKit 窗口操作:原生层无法通过 UIKit 的 API 让窗口进入系统全屏,必须绕过 UIKit,直接向 AppKit 的 NSWindow 发送 toggleFullScreen: 消息,让它进入或退出独立的桌面空间。

同时,原生层在收到消息后需要检查窗口当前的实际全屏状态,避免重复切换导致窗口来回缩放。

方案架构

整体分为 WebView 层和原生层两块。交互流程如下:

sequenceDiagram actor User participant Video as video 元素 participant JS as 注入脚本 participant Handler as WKScriptMessageHandler participant Native as CSharp 原生层 participant NSWindow as AppKit NSWindow User->>Video: 点击全屏按钮 Video->>Video: 视频在 WebView 内占满 Video-->>JS: 触发 webkitbeginfullscreen JS->>Handler: postMessage(true) Handler->>Native: DidReceiveScriptMessage (true) Native-->>NSWindow: 检查 styleMask,若未全屏则 toggleFullScreen: NSWindow-->>NSWindow: 窗口进入系统全屏 Space User->>Video: 退出全屏 (按钮/ESC) Video->>Video: 视频结束内嵌全屏 Video-->>JS: 触发 webkitendfullscreen JS->>Handler: postMessage(false) Handler->>Native: DidReceiveScriptMessage (false) Native-->>NSWindow: 检查 styleMask,若仍全屏则 toggleFullScreen: NSWindow-->>NSWindow: 窗口退出系统全屏

JavaScript 层实现

BlazorWebViewInitializing 事件中拿到 WKUserContentController,注册消息处理器,并注入监听脚本。

e.Configuration.UserContentController.AddScriptMessageHandler(
    new FullscreenHandler(), "fullscreenHandler");

e.Configuration.UserContentController.AddUserScript(
    new WKUserScript(new NSString(@"
        const notifyFullscreenChange = (isEnterFullscreen) => {
            window.webkit.messageHandlers.fullscreenHandler.postMessage(isEnterFullscreen);
        };
        document.addEventListener('webkitbeginfullscreen', () => notifyFullscreenChange(true), true);
        document.addEventListener('webkitendfullscreen', () => notifyFullscreenChange(false), true);
    "), WKUserScriptInjectionTime.AtDocumentEnd, true));

几个容易出问题的地方:

  • 事件监听使用捕获阶段(addEventListener 第三个参数 true)。如果漏掉这个参数,事件可能被内部元素阻止冒泡而丢失,表现为全屏按钮点了没反应。排查时遇到过这种情况,加回来后恢复正常。
  • 消息体直接传布尔值。truefalse 明确表达进入和退出,原生层不需要额外维护状态机去猜测意图。
  • 若页面内有

原文地址: https://www.cveoy.top/t/topic/qGvI 著作权归作者所有。请勿转载和采集!

免费AI点我,无需注册和登录