1# 同层渲染绘制Video和Button组件 2 3Web组件支持同层渲染绘制Video和Button组件。 4 5开发者可通过[enableNativeEmbedMode()](../reference/apis-arkweb/ts-basic-components-web.md#enablenativeembedmode11)控制同层渲染开关。Html文件中需要显式使用embed标签,并且embed标签内type必须以“native/”开头。 6 7 8同层渲染的标签背景是白色的,只支持Web组件嵌套一层Web组件。 9 10 11- 使用前请在module.json5添加如下权限。 12 13 ```ts 14 "ohos.permission.INTERNET" 15 ``` 16 17- 应用侧代码,同层渲染组件使用示例。 18 19 ```ts 20 // 创建NodeController 21 import webview from '@ohos.web.webview'; 22 import {UIContext} from '@ohos.arkui.UIContext'; 23 import {NodeController, BuilderNode, NodeRenderType, FrameNode} from "@ohos.arkui.node"; 24 import {AVPlayerDemo} from './PlayerDemo'; 25 26 declare class Params { 27 textOne : string 28 textTwo : string 29 width : number 30 height : number 31 } 32 declare class nodeControllerParams { 33 surfaceId : string 34 type : string 35 renderType : NodeRenderType 36 embedId : string 37 width : number 38 height : number 39 } 40 // 用于控制和反馈对应的NodeContainer上的节点的行为,需要与NodeContainer一起使用。 41 class MyNodeController extends NodeController { 42 private rootNode: BuilderNode<[Params]> | undefined | null; 43 private embedId_ : string = ""; 44 private surfaceId_ : string = ""; 45 private renderType_ :NodeRenderType = NodeRenderType.RENDER_TYPE_DISPLAY; 46 private width_ : number = 0; 47 private height_ : number = 0; 48 private type_ : string = ""; 49 50 setRenderOption(params : nodeControllerParams) { 51 this.surfaceId_ = params.surfaceId; 52 this.renderType_ = params.renderType; 53 this.embedId_ = params.embedId; 54 this.width_ = params.width; 55 this.height_ = params.height; 56 this.type_ = params.type; 57 } 58 // 必须要重写的方法,用于构建节点数、返回节点数挂载在对应NodeContainer中。 59 // 在对应NodeContainer创建的时候调用、或者通过rebuild方法调用刷新。 60 makeNode(uiContext: UIContext): FrameNode | null{ 61 this.rootNode = new BuilderNode(uiContext, { surfaceId: this.surfaceId_, type: this.renderType_}); 62 if (this.type_ === 'native/button') { 63 this.rootNode.build(wrapBuilder(ButtonBuilder), {textOne: "myButton1", textTwo : "myButton2", width : this.width_, height : this.height_}); 64 } else if (this.type_ === 'native/video') { 65 this.rootNode.build(wrapBuilder(VideoBuilder), {textOne: "myButton", width : this.width_, height : this.height_}); 66 } else { 67 // other 68 } 69 // 返回FrameNode节点。 70 return this.rootNode.getFrameNode(); 71 } 72 73 setBuilderNode(rootNode: BuilderNode<Params[]> | null): void{ 74 this.rootNode = rootNode; 75 } 76 77 getBuilderNode(): BuilderNode<[Params]> | undefined | null{ 78 return this.rootNode; 79 } 80 81 updateNode(arg: Object): void { 82 this.rootNode?.update(arg); 83 } 84 getEmbedId() : string { 85 return this.embedId_; 86 } 87 88 postEvent(event: TouchEvent | undefined) : boolean { 89 return this.rootNode?.postTouchEvent(event) as boolean 90 } 91 } 92 93 @Component 94 struct ButtonComponent { 95 @Prop params: Params 96 @State bkColor: Color = Color.Red 97 98 build() { 99 Column() { 100 Button(this.params.textOne) 101 .height(50) 102 .width(200) 103 .border({ width: 2, color: Color.Red}) 104 .backgroundColor(this.bkColor) 105 106 Button(this.params.textTwo) 107 .height(50) 108 .width(200) 109 .border({ width: 2, color: Color.Red}) 110 .backgroundColor(this.bkColor) 111 } 112 .width(this.params.width) 113 .height(this.params.height) 114 } 115 } 116 117 @Component 118 struct VideoComponent { 119 @Prop params: Params 120 @State bkColor: Color = Color.Red 121 testController: WebviewController = new webview.WebviewController(); 122 mXComponentController: XComponentController = new XComponentController(); 123 @State player_changed: boolean = false; 124 player?: AVPlayerDemo; 125 126 build() { 127 Column() { 128 Button(this.params.textOne) 129 .height(50) 130 .width(100) 131 .border({ width: 2, color: Color.Red}) 132 .backgroundColor(this.bkColor) 133 134 XComponent({ id: 'video_player_id', type: XComponentType.SURFACE, controller: this.mXComponentController}) 135 .width(300) 136 .height(300) 137 .border({width: 1, color: Color.Red}) 138 .onLoad(() => { 139 this.player = new AVPlayerDemo(); 140 this.player.setSurfaceID(this.mXComponentController.getXComponentSurfaceId()); 141 this.player_changed = !this.player_changed; 142 this.player.avPlayerLiveDemo() 143 }) 144 } 145 .width(this.params.width) 146 .height(this.params.height) 147 } 148 } 149 // @Builder中为动态组件的具体组件内容。 150 @Builder 151 function ButtonBuilder(params: Params) { 152 ButtonComponent({ params: params }) 153 .backgroundColor(Color.Green) 154 } 155 156 @Builder 157 function VideoBuilder(params: Params) { 158 VideoComponent({ params: params }) 159 .backgroundColor(Color.Green) 160 } 161 162 @Entry 163 @Component 164 struct WebIndex { 165 browserTabController: WebviewController = new webview.WebviewController() 166 private nodeControllerMap: Map<string, MyNodeController> = new Map(); 167 @State componentIdArr: Array<string> = []; 168 169 aboutToAppear() { 170 // 配置web开启调试模式。 171 webview.WebviewController.setWebDebuggingAccess(true); 172 } 173 174 build(){ 175 Row() { 176 Column({ space: 5}) { 177 Stack() { 178 ForEach(this.componentIdArr, (componentId: string) => { 179 NodeContainer(this.nodeControllerMap.get(componentId)) 180 }, (embedId: string) => embedId) 181 // web组件加载本地test.html页面。 182 Web({ src: $rawfile("test.html"), controller: this.browserTabController }) 183 // 配置同层渲染开关开启。 184 .enableNativeEmbedMode(true) 185 // 获取embed标签的生命周期变化数据。 186 .onNativeEmbedLifecycleChange((embed) => { 187 console.log("NativeEmbed surfaceId" + embed.surfaceId); 188 // 获取web侧embed元素的id。 189 const componentId = embed.info?.id?.toString() as string 190 if (embed.status == NativeEmbedStatus.CREATE) { 191 console.log("NativeEmbed create" + JSON.stringify(embed.info)) 192 // 创建节点控制器,设置参数并rebuild。 193 let nodeController = new MyNodeController() 194 nodeController.setRenderOption({surfaceId : embed.surfaceId as string, type : embed.info?.type as string, renderType : NodeRenderType.RENDER_TYPE_TEXTURE, embedId : embed.embedId as string, width : px2vp(embed.info?.width), height : px2vp(embed.info?.height)}) 195 nodeController.rebuild() 196 // 根据web传入的embed的id属性作为key,将nodeController存入map。 197 this.nodeControllerMap.set(componentId, nodeController) 198 // 将web传入的embed的id属性存入@State状态数组变量中,用于动态创建nodeContainer节点容器,需要将push动作放在set之后。 199 this.componentIdArr.push(componentId) 200 } else if (embed.status == NativeEmbedStatus.UPDATE) { 201 let nodeController = this.nodeControllerMap.get(componentId) 202 nodeController?.updateNode({text: 'update', width: px2vp(embed.info?.width), height: px2vp(embed.info?.height)} as ESObject) 203 nodeController?.rebuild() 204 } else { 205 let nodeController = this.nodeControllerMap.get(componentId) 206 nodeController?.setBuilderNode(null) 207 nodeController?.rebuild() 208 } 209 })// 获取同层渲染组件触摸事件信息。 210 .onNativeEmbedGestureEvent((touch) => { 211 console.log("NativeEmbed onNativeEmbedGestureEvent" + JSON.stringify(touch.touchEvent)); 212 this.componentIdArr.forEach((componentId: string) => { 213 let nodeController = this.nodeControllerMap.get(componentId) 214 if (nodeController?.getEmbedId() === touch.embedId) { 215 let ret = nodeController?.postEvent(touch.touchEvent) 216 if (ret) { 217 console.log("onNativeEmbedGestureEvent success " + componentId) 218 } else { 219 console.log("onNativeEmbedGestureEvent fail " + componentId) 220 } 221 } 222 }) 223 }) 224 } 225 } 226 } 227 } 228 } 229 ``` 230- 应用侧代码,视频播放示例。 231 232 ```ts 233 import media from '@ohos.multimedia.media'; 234 import {BusinessError} from '@ohos.base'; 235 236 export class AVPlayerDemo { 237 private count: number = 0; 238 private surfaceID: string = ''; // surfaceID用于播放画面显示,具体的值需要通过Xcomponent接口获取,相关文档链接见上面Xcomponent创建方法。 239 private isSeek: boolean = true; // 用于区分模式是否支持seek操作。 240 241 setSurfaceID(surface_id: string){ 242 console.log('setSurfaceID : ' + surface_id); 243 this.surfaceID = surface_id; 244 } 245 // 注册avplayer回调函数。 246 setAVPlayerCallback(avPlayer: media.AVPlayer) { 247 // seek操作结果回调函数。 248 avPlayer.on('seekDone', (seekDoneTime: number) => { 249 console.info(`AVPlayer seek succeeded, seek time is ${seekDoneTime}`); 250 }) 251 // error回调监听函数,当avplayer在操作过程中出现错误时,调用reset接口触发重置流程。 252 avPlayer.on('error', (err: BusinessError) => { 253 console.error(`Invoke avPlayer failed, code is ${err.code}, message is ${err.message}`); 254 avPlayer.reset(); 255 }) 256 // 状态机变化回调函数。 257 avPlayer.on('stateChange', async (state: string, reason: media.StateChangeReason) => { 258 switch (state) { 259 case 'idle': // 成功调用reset接口后触发该状态机上报。 260 console.info('AVPlayer state idle called.'); 261 avPlayer.release(); // 调用release接口销毁实例对象。 262 break; 263 case 'initialized': // avplayer 设置播放源后触发该状态上报。 264 console.info('AVPlayer state initialized called.'); 265 avPlayer.surfaceId = this.surfaceID; // 设置显示画面,当播放的资源为纯音频时无需设置。 266 avPlayer.prepare(); 267 break; 268 case 'prepared': // prepared调用成功后上报该状态机。 269 console.info('AVPlayer state prepared called.'); 270 avPlayer.play(); // 调用播放接口开始播放。 271 break; 272 case 'playing': // play成功调用后触发该状态机上报。 273 console.info('AVPlayer state prepared called.'); 274 if(this.count !== 0) { 275 if (this.isSeek) { 276 console.info('AVPlayer start to seek.'); 277 avPlayer.seek(avPlayer.duration); // seek到视频末尾。 278 } else { 279 // 当播放模式不支持seek操作时继续播放到结尾。 280 console.info('AVPlayer wait to play end.'); 281 } 282 } else { 283 avPlayer.pause(); // 调用暂停接口暂停播放。 284 } 285 this.count++; 286 break; 287 case 'paused': // pause成功调用后触发该状态机上报。 288 console.info('AVPlayer state paused called.'); 289 avPlayer.play(); // 再次播放接口开始播放。 290 break; 291 case 'completed': //播放接口后触发该状态机上报。 292 console.info('AVPlayer state paused called.'); 293 avPlayer.stop(); // 调用播放接口接口。 294 break; 295 case 'stopped': // stop接口后触发该状态机上报。 296 console.info('AVPlayer state stopped called.'); 297 avPlayer.reset(); // 调用reset接口初始化avplayer状态。 298 break; 299 case 'released': //播放接口后触发该状态机上报。 300 console.info('AVPlayer state released called.'); 301 break; 302 default: 303 break; 304 } 305 }) 306 } 307 308 // 通过url设置网络地址来实现播放直播码流。 309 async avPlayerLiveDemo(){ 310 // 创建avPlayer实例对象 311 let avPlayer: media.AVPlayer = await media.createAVPlayer(); 312 // 创建状态机变化回调函数。 313 this.setAVPlayerCallback(avPlayer); 314 this.isSeek = false; // 不支持seek操作。 315 avPlayer.url = 'https://xxx.xxx/demo.mp4'; 316 } 317 } 318 ``` 319 320- 前端页面示例。 321 322 ```html 323 <!Document> 324 <html> 325 <head> 326 <title>同层渲染测试html</title> 327 <meta name="viewport"> 328 </head> 329 <body> 330 <div> 331 <div id="bodyId"> 332 <embed id="nativeButton" type = "native/button" width="800" height="800" src="test?params1=xxx?" style = "background-color:red"/> 333 </div> 334 <div id="bodyId1"> 335 <embed id="nativeVideo" type = "native/video" width="500" height="500" src="test" style = "background-color:red"/> 336 </div> 337 </div> 338 <div id="button" width="500" height="200"> 339 <p>bottom</p> 340 </div> 341 <script> 342 let nativeEmbed = { 343 // 判断设备是否支持touch事件。 344 nativeButton : document.getElementById('nativeButton'), 345 nativeVideo : document.getElementById('nativeVideo'), 346 347 // 事件。 348 events:{}, 349 // 初始化。 350 init:function(){ 351 let self = this; 352 self.nativeButton.addEventListener('touchstart', self.events, false); 353 self.nativeVideo.addEventListener('touchstart', self.events, false); 354 } 355 }; 356 nativeEmbed.init(); 357 </script> 358 359 </body> 360 </html> 361 ``` 362 363 