先聊一个查了半天的坑。客户那边拿鸿蒙应用的Web组件加载H5页面页面里放了一段视频网页在手机浏览器里打开一切正常点全屏按钮就能横屏播放。放进鸿蒙应用里视频能播、声音也有就是点全屏没反应或者闪一下就退出来偶尔还会出现黑屏但有声的怪状态。查日志、翻文档、各种试错之后问题基本锁定在三个层面鸿蒙Web组件的全屏权限没开、H5页面video标签和全屏API的写法不兼容、还有全屏生命周期回调里没有同步处理窗口旋转和沉浸式布局。这篇文章我把排查过程和修复方案完整写一遍给做鸿蒙应用内嵌H5业务的同学当个参考。1. 全屏失效的表现与根因拆解1.1 四种典型失效现象先对号入座“全屏失效”这四个字其实很笼统实际开发中会遇到完全不同的表现。我整理了一下大部分情况可以归为下面四类第一种点击全屏按钮完全没反应。视频正常播放按钮也能点但点了之后页面纹丝不动。这种通常不是H5代码的问题是鸿蒙侧Web组件压根没把全屏能力授权给网页。第二种进入全屏后黑屏但有声音。这个最迷惑人。看起来像进入了全屏状态但画面没了。问题大概率出在视频解码和渲染环节比如媒体权限没开、视频编码格式系统不支持、或者Web组件的媒体渲染通道被其它上层组件遮挡。第三种全屏只能坚持一两秒然后自动退出。这个和全屏生命周期有关。H5页面调用requestFullscreen之后会触发fullscreenchange事件鸿蒙侧Web组件也会回调全屏状态。如果这个回调时机和窗口旋转逻辑冲突就可能出现“刚进入全屏就被打断”的现象。第四种视频画面放大了但状态栏和导航栏还在看起来不是真正的沉浸式全屏。这种情况属于半全屏根本原因是窗口没有切换成沉浸式布局页面元素占了全屏但是系统栏还悬浮在上面。把现象对准之后再动手改代码会省很多时间。千万不要一上来就各种加参数、改旋转逻辑先搞清楚属于哪一种。1.2 全屏是一条链路不是video标签一个点的事很多同学容易把全屏理解成“网页自己把自己放大了”其实在鸿蒙Web组件里全屏是一整条链路共同协作的结果。简化一下这条链路H5页面的video标签或者document元素调用Fullscreen API请求进入全屏。Web内核ArkWeb基于Chromium拦截到这个请求判断当前Web组件是否允许全屏。如果允许就回调给鸿蒙侧宿主应用比如触发onFullScreenChange事件。接着鸿蒙侧应用要配合做三件事把宿主窗口旋转到横屏或者保持当前方向看产品需求、隐藏状态栏和导航栏进入沉浸式布局、让Web组件占满整个窗口。最后全屏退出的时候还要反向执行一遍恢复窗口方向和系统栏。这条链路里任何一环断了表现出来的就是各种“失效”。比如第一环没授权就是点击没反应最后一环没执行就是半全屏如果旋转时机和全屏回调时机冲突就会出现闪退。打个比方舞台上的演员视频准备开演了但是灯控全屏权限没给信号舞台升降窗口切换没动音响控制媒体权限也没接上那这场戏肯定是演不起来的。所以在排查的时候一定要顺着链路一段一段检查而不是只盯着视频标签看。1.3 为什么鸿蒙Web组件默认不放行全屏有一个问题值得多说一句为什么网页在浏览器里全屏好好的放进鸿蒙Web组件就要额外授权因为Web组件本身是宿主应用的一部分网页请求全屏本质上是在请求“脱离普通页面展示模式”把整个应用窗口的控制权临时交给网页。这对宿主应用来说是一件需要明确授权的事情。你可以想象一下如果网页能随意全屏、随意旋转、随意隐藏系统栏那用户很容易被诱导明明在看内容网页某个弹窗一点就切到全屏状态栏没了退出按钮藏在角落里。这种体验和安全都是有问题的。所以鸿蒙ArkWeb延续了Chromium内核“宿主授权”的设计思路。Web组件有一个明确的属性开关默认关闭开发者确认自己的业务需要承载全屏视频或者全屏游戏页面时再显式打开。老Android开发者看到这个设计应该很眼熟和Android WebView里onShowCustomView那套机制是同一个理念只是鸿蒙把它收敛成了一个配置项加一组回调用起来更直接。2. 鸿蒙侧Web组件的权限配置先把门打开2.1 一分钟搭建带Web组件的基线和四个关键开关先把最基础的工程配置写好。下面的代码是一个带Web组件的页面可以直接跑。重点看四个属性javaScriptAccess、domStorageAccess、mediaAccess、allowFullScreen。import web from ohos.web.webview; Entry Component struct VideoWebPage { controller: web.WebviewController new web.WebviewController(); build() { Column() { Web({ src: https://your-domain.com/video.html, controller: this.controller }) .javaScriptAccess(true) .domStorageAccess(true) .mediaAccess(true) .allowFullScreen(true) } .width(100%) .height(100%) } }这四个开关的职责要弄清楚不然以后调别的视频问题还会懵。javaScriptAccess控制网页脚本执行没有它页面里的JS逻辑都跑不起来。domStorageAccess控制localStorage、sessionStorage和IndexedDB很多播放器脚本会往本地存播放进度、音量设置、静音状态不开放这个播放器初始化可能异常。mediaAccess管理音视频采集和播放权限视频能不能出声、画面能不能渲染它说了算。allowFullScreen就是上面讲的全屏总开关。实际开发里mediaAccess这个最容易被漏掉。很多时候视频能播、声音能出来是因为媒体权限在Web组件层面没有限制但某些编码格式或者特殊播放逻辑会额外需要这个授权。我的建议是做视频类页面这四个开关全部打开省得后面一个一个踩。2.2 全屏事件回调进入和退出时要处理的窗口逻辑Web组件只是打开了全屏的“门”真正让用户觉得“全屏成功”的是后面的窗口配合。鸿蒙侧需要监听全屏事件在进入全屏时旋转窗口、隐藏系统栏在退出全屏时恢复原状。下面是一段典型的处理逻辑配合前面那个基础页面import web from ohos.web.webview; import window from ohos.window; import { BusinessError } from ohos.base; Entry Component struct VideoWebPage { controller: web.WebviewController new web.WebviewController(); private mainWindow: window.Window | null null; aboutToAppear() { window.getLastWindow(getContext(this)).then((mainWindow: window.Window) { this.mainWindow mainWindow; }).catch((err: BusinessError) { console.error(getLastWindow failed, code: err.code , msg: err.message); }); } build() { Column() { Web({ src: https://your-domain.com/video.html, controller: this.controller }) .javaScriptAccess(true) .domStorageAccess(true) .mediaAccess(true) .allowFullScreen(true) .onFullScreenChange((event) { if (event.isEnterFullScreen) { this.enterFullScreen(); } else { this.exitFullScreen(); } }) } .width(100%) .height(100%) } private enterFullScreen() { if (!this.mainWindow) { return; } this.mainWindow.setPreferredOrientation(window.Orientation.LANDSCAPE); this.mainWindow.setWindowLayoutFullScreen(true); this.mainWindow.setWindowSystemBarEnable([]); } private exitFullScreen() { if (!this.mainWindow) { return; } this.mainWindow.setPreferredOrientation(window.Orientation.PORTRAIT); this.mainWindow.setWindowLayoutFullScreen(false); this.mainWindow.setWindowSystemBarEnable([status, navigation]); } }这里有一个细节要强调全屏方向不要自己写死。H5页面里的视频可能横屏、可能竖屏视频内容决定全屏布局鸿蒙侧宿主应用只能配合执行不能替网页做决定。所以我上面示例用了横屏只是演示窗口操作能力。真实业务中应该通过判断event对象里带的信息或者和前端约好方向策略再决定旋转到哪个方向。2.3 页面退出和路由跳转一个容易忽略的全屏恢复很多团队在调试的时候视频全屏是好的但退出视频页面时会出问题明明退出了当前页面状态栏还是隐藏的或者上一个页面变成了横屏。这个问题的本质是页面生命周期和全屏生命周期没有同步处理。正确做法是在页面销毁或者隐藏时主动调用Web组件的退出全屏接口把状态恢复干净。aboutToDisappear() { this.exitFullScreen(); this.controller.exitFullScreen(); }如果你用的是Navigation导航页面通过navigateTo跳转目标页进入后上一个页面仍然存活只是被覆盖。这种时候被覆盖页面里的Web组件如果还处于全屏状态就可能出现窗口状态错乱。建议在onPageHide里也补上同样的退出逻辑。另外应用退到后台之前也应当处理一下不然从后台回来可能还是全屏状态用户视角会非常困惑。还有一个多实例的坑如果你的项目里同一个页面被多次push或者Web组件所在的组件被条件渲染销毁重建全屏状态会跟着实例一起丢。所以保存窗口引用的操作要跟着页面生命周期走不要在页面销毁后还去调用已经失效的窗口引用。3. H5页面侧的兼容检查与修复3.1 video标签上的playsinline属性很多时候全屏失效的源头在这儿鸿蒙Web组件的权限都打开了窗口配合也写了但视频全屏还是有问题。这时候就该回头检查H5页面本身了。先看video标签。移动端网页播放视频有一个特别的属性叫playsinline部分旧内核还需要webkit-playsinline。它的作用是告诉浏览器别把视频弹出去用系统播放器就在页面内联播放。为什么不设置它会导致全屏失效因为一旦系统播放器接管视频HTML5的Fullscreen API路径就被绕开了。你点页面上的全屏按钮触发的可能是Native播放器的那套全屏逻辑而不是网页自身的requestFullscreen调用。鸿蒙侧Web组件监听不到预期的事件自然没法配合做窗口旋转表现就是“全屏不了”或者行为异常。建议video标签加上如下配置video idvideoPlayer srchttps://your-domain.com/video.mp4 controls playsinline webkit-playsinline preloadmetadata x5-video-player-typeh5 x5-video-player-fullscreentrue /video关于x5-video-player-type和x5-video-player-fullscreen要说明一下这是腾讯X5内核的历史遗留参数在鸿蒙ArkWeb这种Chromium内核下不会生效但写了也不会报错。如果你这个页面还要复用在其它安卓壳里建议保留。如果只跑鸿蒙Web组件删不删影响不大。3.2 Fullscreen API的标准写法和手势限制全屏请求的代码虽然简单但兼容性写法还是有讲究。不少老页面还在用webkitRequestFullscreen或者webkitEnterFullScreenArkWeb作为Chromium系内核标准接口是requestFullscreen。为了多端复用可以做一个兼容函数function enterFullscreen(element) { if (element.requestFullscreen) { element.requestFullscreen(); } else if (element.webkitRequestFullscreen) { element.webkitRequestFullscreen(); } else if (element.msRequestFullscreen) { element.msRequestFullscreen(); } }另一个很关键的限制是“用户手势”。Fullscreen API要求必须有用户激活user activation才能调用也就是说requestFullscreen必须在用户的点击、触摸等事件处理函数里执行。你不能在视频加载完成后的loadedmetadata回调里全屏也不能在定时器里全屏。如果违反这个规则浏览器会抛异常API can only be initiated by a user gesture。排查的时候如果看到webview控制台输出这类报错先检查是不是页面的全屏按钮用了异步逻辑比如点击之后先发一个埋点请求然后在埋点回调里再去调全屏。这样一顿操作用户激活上下文已经丢了全屏必然失败。3.3 在H5侧监听fullscreenchange快速判断全屏事件有没有触发当排查陷入僵局时我习惯在H5侧加监听器把全屏事件的发生过程完整打印出来。这一步能快速区分问题在H5侧还是鸿蒙侧。document.addEventListener(fullscreenchange, function() { console.log(fullscreenchange, isFullScreen:, document.fullscreenElement ? yes : no); console.log(fullscreenElement:, document.fullscreenElement ? document.fullscreenElement.tagName : null); }); document.addEventListener(webkitfullscreenchange, function() { console.log(webkitfullscreenchange, isFullScreen:, document.webkitFullscreenElement ? yes : no); });如果点击全屏按钮后控制台根本没有fullscreenchange日志说明H5侧全屏请求没发出来或者被浏览器拦了。如果有日志说明H5侧问题不大需要到鸿蒙侧继续查。3.4 监听全屏事件时要注意时序问题还有一个容易被忽略的坑退出全屏操作会按“后进先出”的顺序逐个触发。很多播放器自己会去调exitFullscreen页面脚本在fullscreenchange里也保存了一个状态。如果同时有两个全屏请求叠在一起退一层还会剩一层。这就是为什么有些页面“第一次退出全屏后页面还是全屏的”。视频类页面尤其容易出现这种状态因为video自身的controls全屏按钮和页面自定义的全屏按钮会分别走各自的逻辑。建议在H5侧维护一个全屏状态队列或者干脆统一由页面自定义全屏按钮接管把原生controls里的全屏功能用controlsListnofullscreen关掉。video controls controlsListnofullscreen playsinline/video这样能避免用户一边点系统自带的全屏按钮一边点页面的全屏按钮导致状态错乱。4. 完整可运行的方案从H5测试页到鸿蒙容器4.1 一个可以直接用来复现问题的H5测试页如果你和我一样喜欢“最小可复现demo”的工作方式可以做一个非常干净的测试页。它只有一个video标签、一个自定义全屏按钮并把所有全屏相关事件打印出来。!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title鸿蒙Web组件全屏测试/title style body { margin: 0; background: #222; } .player { width: 100%; } video { width: 100%; background: #000; } .full-btn { display: block; width: 200px; margin: 20px auto; padding: 10px 0; text-align: center; background: #ff6600; color: #fff; border: none; border-radius: 6px; } /style /head body video idv classplayer controls playsinline webkit-playsinline preloadmetadata source srchttps://media.w3.org/2010/05/sintel/trailer.mp4 typevideo/mp4 /video button classfull-btn onclickfullscreenFn()全屏播放/button script function fullscreenFn() { const v document.getElementById(v); if (v.requestFullscreen) { v.requestFullscreen(); } else if (v.webkitRequestFullscreen) { v.webkitRequestFullscreen(); } else { console.log(no requestFullscreen support); } } document.addEventListener(fullscreenchange, function() { console.log(fullscreenchange, fullscreenElement:, document.fullscreenElement ? document.fullscreenElement.tagName : null); }); /script /body /html这个页面里的https://media.w3.org/2010/05/sintel/trailer.mp4是W3C提供的公开测试视频可以放心用。把这个HTML放到你的H5服务器上或者把它作为rawfile文件内置到鸿蒙应用里便于离线复现。4.2 鸿蒙侧的完整容器代码接下来是鸿蒙侧的完整代码。我习惯把窗口操作封装成一个类这样页面代码会干净很多。下面的示例是所有代码都写在页面组件里但逻辑清楚适合当模板。import web from ohos.web.webview; import window from ohos.window; import { BusinessError } from ohos.base; Entry Component struct VideoWebPage { controller: web.WebviewController new web.WebviewController(); private mainWindow: window.Window | null null; aboutToAppear() { window.getLastWindow(getContext(this)).then((mainWindow: window.Window) { this.mainWindow mainWindow; }).catch((err: BusinessError) { console.error(getLastWindow failed, code: err.code , msg: err.message); }); } aboutToDisappear() { this.exitFullScreen(); } build() { Column() { Web({ src: https://your-domain.com/video.html, controller: this.controller }) .javaScriptAccess(true) .domStorageAccess(true) .mediaAccess(true) .allowFullScreen(true) .fileAccess(true) .onFullScreenChange((event) { if (event.isEnterFullScreen) { this.enterFullScreen(); } else { this.exitFullScreen(); } }) } .width(100%) .height(100%) } private enterFullScreen() { if (!this.mainWindow) { return; } this.mainWindow.setPreferredOrientation(window.Orientation.LANDSCAPE); this.mainWindow.setWindowLayoutFullScreen(true); this.mainWindow.setWindowSystemBarEnable([]); } private exitFullScreen() { if (!this.mainWindow) { return; } this.mainWindow.setPreferredOrientation(window.Orientation.PORTRAIT); this.mainWindow.setWindowLayoutFullScreen(false); this.mainWindow.setWindowSystemBarEnable([status, navigation]); } }有一点要提醒如果你把测试页面内置到了应用资源里用$rawfile(video.html)作为src那么页面里引用同目录下的视频资源也需要放进去。不过我的建议是测试阶段直接用线上H5地址因为这样能一并验证网络权限、域名校验、HTTPS证书等真实场景的问题。4.3 实操验证按顺序测五个场景代码就位之后按下面的顺序验证每个场景单独测避免互相干扰。第一个场景直接加载测试页视频能播放但不点全屏预期是正常内联播放。如果不能播放先查网络权限和视频编码。第二个场景点击页面上的自定义全屏按钮预期是进入横屏全屏、状态栏和导航栏隐藏。如果没反应检查鸿蒙侧allowFullScreen有没有开以及H5控制台有没有全屏报错。第三个场景全屏状态下旋转设备方向预期页面跟随旋转视频保持全屏。如果不跟随可能是窗口方向锁死了。第四个场景点击系统的退出全屏按钮或按返回键预期退出全屏、恢复竖屏和系统栏。如果恢复不完整检查exitFullScreen里窗口恢复逻辑有没有执行。第五个场景全屏状态下退到后台再回前台预期保持全屏或者自动退出全屏但不能出现状态栏丢失或者页面变形。这一步很多团队会遗漏但用户实际使用中非常常见。这五个场景全部通过基本可以认为全屏链路是通的。剩下的就是和产品确认某些页面是否需要一直横屏某些页面是否需要禁止旋转这些属于产品策略代码上都已经具备调整能力。5. 常见问题速查表与实战避坑5.1 问题速查表把这次排查中遇到的典型问题整理成一个速查表方便后续团队快速定位。现象可能原因解决方案点击全屏按钮没反应鸿蒙侧allowFullScreen未开启Web组件添加.allowFullScreen(true)全屏黑屏但有声音媒体权限未开或视频编码不支持添加.mediaAccess(true)检查视频编码换h264测试全屏一两秒自动退出全屏事件回调与窗口旋转时序冲突在onFullScreenChange里统一处理避免H5侧重复调用exitFullscreen画面半全屏但系统栏还在未进入沉浸式布局调用setWindowLayoutFullScreen(true)并隐藏系统栏退出全屏后页面还是全屏多层全屏请求叠加未清空用controlsListnofullscreen屏蔽原生全屏按钮统一走页面全屏逻辑返回上个页面横屏回不来页面销毁时没恢复窗口状态aboutToDisappear或onPageHide里主动恢复窗口方向和系统栏视频弹出系统播放器无法内联缺少playsinline属性video标签添加playsinline webkit-playsinline5.2 前后台切换和全屏状态管理应用退到后台这个场景是实际使用中反馈最多的问题之一。用户在全屏状态下按Home键退到桌面再回到应用发现界面是横屏的状态栏却回来了或者视频变成一个小窗。我的建议是做一个统一的全屏状态管理器。这个模块不在本文展开写了核心思路就是进入全屏时保存当前状态页面onHide时根据状态决定是否退出全屏页面重新onShow时再恢复。如果你不想做这么重最简单粗暴的方案就是监听应用前后台状态退后台一律退出全屏。虽然损失一点体验但至少状态不会错乱。还有一个细节Web组件的onFullScreenChange回调里event.isEnterFullScreen在不同API版本上可能字段名有差异。旧版本的ArkWeb用onFullScreen回调只在进入全屏时触发一次。如果你们的SDK比较老看到文档里没有onFullScreenChange就换成onFullScreen加exitFullScreen()手动退出逻辑是一样的只是监听方式不同。5.3 调试利器打开Web调试和日志过滤排查这类问题一定要把Web组件的调试能力打开。鸿蒙Web组件有一个setWebDebuggingAccess(true)属性设置之后就可以通过一些调试工具去查看网页控制台和DOM状态。Web({ src: https://your-domain.com/video.html, controller: this.controller }) .setWebDebuggingAccess(true)打开调试之后结合DevEco Studio的Log窗口过滤关键字ArkWeb、FullScreen、JSFullScreen可以看到网页和Web组件的关键事件。这里有个经验很多全屏问题在网页控制台里其实是有报错信息的只是鸿蒙应用的日志里不显眼。我遇到过最典型的就是requestFullscreen() must be called from a user gesture这种报错信息如果不看网页控制台靠猜的话能猜一个下午。5.4 多端复用场景把全屏能力封装成桥接接口现在很多团队都在做多端复用同一套H5页面既要跑鸿蒙App又要跑小程序还要兼容浏览器。这种场景下全屏逻辑如果直接写在页面脚本里到了各端还要单独适配。更合理的做法是在H5侧定义一个统一的全屏能力接口由各端的宿主容器实现。举个例子你可以约定一个window.CapabilityBridge对象暴露playFullscreen(videoElement)和exitFullscreen()两个方法。鸿蒙端在网页加载前注入实现内部调用全屏API并配合窗口旋转小程序端注入自己的一套实现。这样H5业务代码只需要调用CapabilityBridge.playFullscreen()不用关心底层容器差异。这个思路同样适用于其它Web组件能力比如键盘弹起问题。热搜词里提到“app内嵌h5页面点击input自动滑动到对应input显示键盘”这类问题的本质也是Web组件与原生输入法的交互链路。把这类交互统一收敛到桥接层比在每个页面上打补丁要可靠得多。5.5 最后再说一个容易被忽略的媒体权限细节排查全屏问题的时候还有一个隐藏条件容易被漏掉应用本身的媒体权限声明。如果在module.json5里没有声明ohos.permission.MICROPHONE或者其它媒体相关权限部分机型在特定场景下会导致Web组件内部媒体模块初始化异常。表现就是视频能播但全屏后的媒体渲染会出问题。如果你的测试机恰好是某几个特定型号复现黑屏建议先检查权限声明再检查代码逻辑。权限声明这一步虽然是基础工作但很多“偶现”问题最后都栽在它上面。结尾这篇文章里的代码和排查思路是我在多轮实际调试中沉淀下来的。如果你也遇到类似的全屏失效问题我强烈建议先做两件事打开Web调试给H5页面加上fullscreenchange监听打印事件日志判断问题出在H5层还是鸿蒙层对照速查表快速检查鸿蒙侧Web组件的四个关键开关和窗口处理逻辑。九成的问题都能在十分钟内定位到具体环节。最后再分享一个提高效率的小技巧做一套全屏测试playground页面把fullscreenchange、webkitfullscreenchange、resize事件全部打印出来把鸿蒙侧的全屏回调也打印出来。以后不管是新项目还是接手的旧项目遇到类似H5与Web组件的交互问题直接拿这套页面去对照比临时写测试代码快得多。全屏问题看着唬人拆开来看就是权限、生命周期、窗口状态这几个环节的配合理顺了就再也不会被它卡住了。