1# Rendering and Drawing Video and Button Components at the Same Layer 2 3With the same-layer rendering feature of ArkWeb, you can render and draw native components at the same layer as the **\<Web>** component for your application. For details about the components that support same-layer rendering, see [NodeRenderType](../reference/apis-arkui/js-apis-arkui-builderNode.md#noderendertype). 4 5- To start with, add the Internet permission to the **module.json5** file. For details, see [Declaring Permissions in the Configuration File](../security/AccessToken/declare-permissions.md). 6 7 ``` 8 "requestPermissions":[ 9 { 10 "name" : "ohos.permission.INTERNET" 11 } 12 ] 13 ``` 14 15## Constraints 16The following constraints apply when same-layer rendering is used: 17 18- W3C standards-based tags cannot be defined as tags for same-layer rendering. 19 20- The **<object>** tags and **\<embed>** tags cannot be configured for same-layer rendering at the same time. 21 22- To deliver best possible performance, keep the number of tags at the same layer on a page within five. 23 24- The maximum height of tags at the same layer is 8192 px, and the maximum texture size is 8192 px. 25 26- Only one nesting level is supported for the **\<Web>** component. If multiple nesting levels are detected, an error message is displayed. 27 28- The touchscreen events supported in the region for same-layer rendering include swiping, taping, scaling, and long pressing, while dragging is not supported. 29 30- When same-layer rendering is enabled, web pages opened by the **\<Web>** component do not support the pinch gesture or scale APIs, including [initialScale](../reference/apis-arkweb/ts-basic-components-web.md#initialscale), [zoom](../reference/apis-arkweb/js-apis-webview.md#zoom), [zoomIn](../reference/apis-arkweb/js-apis-webview.md#zoomin), and [zoomOut](../reference/apis-arkweb/js-apis-webview.md#zoomout). 31 32- The region for same-layer rendering does not support mouse, keyboard, and touchpad events. 33 34- When same-layer rendering is enabled, web pages opened by the **\<Web>** component do not support the unified rendering mode [RenderMode](../reference/apis-arkweb/ts-basic-components-web.md#rendermode). 35 36 37## Drawing the XComponent+AVPlayer and Button Components 38 39### Enabling Same-Layer Rendering 40 41You can enable or disable same-layer rendering through [enableNativeEmbedMode()](../reference/apis-arkweb/ts-basic-components-web.md#enablenativeembedmode11). To use same-layer rendering, the **\<embed>** element must be explicitly used in the HTML file, and the **type** attribute of the element must start with **native/**. The background of the elements corresponding to the tags at the same layer is transparent. 42 43- Example of using same-layer rendering on the application side: 44 45 ```ts 46 // HAP's src/main/ets/pages/Index.ets 47 // Create a NodeController instance. 48 import { webview } from '@kit.ArkWeb'; 49 import { UIContext, NodeController, BuilderNode, NodeRenderType, FrameNode } from "@kit.ArkUI"; 50 import { AVPlayerDemo } from './PlayerDemo'; 51 52 @Observed 53 declare class Params { 54 textOne : string 55 textTwo : string 56 width : number 57 height : number 58 } 59 60 declare class nodeControllerParams { 61 surfaceId : string 62 type : string 63 renderType : NodeRenderType 64 embedId : string 65 width : number 66 height : number 67 } 68 69 // The NodeController instance must be used with a NodeContainer for controlling and feeding back the behavior of the nodes in the container. 70 class MyNodeController extends NodeController { 71 private rootNode: BuilderNode<[Params]> | undefined | null; 72 private embedId_ : string = ""; 73 private surfaceId_ : string = ""; 74 private renderType_ :NodeRenderType = NodeRenderType.RENDER_TYPE_DISPLAY; 75 private width_ : number = 0; 76 private height_ : number = 0; 77 private type_ : string = ""; 78 private isDestroy_ : boolean = false; 79 80 setRenderOption(params : nodeControllerParams) { 81 this.surfaceId_ = params.surfaceId; 82 this.renderType_ = params.renderType; 83 this.embedId_ = params.embedId; 84 this.width_ = params.width; 85 this.height_ = params.height; 86 this.type_ = params.type; 87 } 88 // Method that must be overridden. It is used to build the number of nodes and return the number of nodes that will be mounted to the corresponding NodeContainer. 89 // Called when the corresponding NodeContainer is created or called by the rebuild method. 90 makeNode(uiContext: UIContext): FrameNode | null{ 91 if (this.isDestroy_) { // rootNode is null. 92 return null; 93 } 94 if (!this.rootNode) { // When rootNode is set to undefined 95 this.rootNode = new BuilderNode(uiContext, { surfaceId: this.surfaceId_, type: this.renderType_}); 96 if (this.type_ === 'native/video') { 97 this.rootNode.build(wrapBuilder(VideoBuilder), {textOne: "myButton", width : this.width_, height : this.height_}); 98 } else { 99 // other 100 } 101 } 102 // Return the FrameNode object. 103 return this.rootNode.getFrameNode(); 104 } 105 106 setBuilderNode(rootNode: BuilderNode<Params[]> | null): void{ 107 this.rootNode = rootNode; 108 } 109 110 getBuilderNode(): BuilderNode<[Params]> | undefined | null{ 111 return this.rootNode; 112 } 113 114 updateNode(arg: Object): void { 115 this.rootNode?.update(arg); 116 } 117 getEmbedId() : string { 118 return this.embedId_; 119 } 120 121 setDestroy(isDestroy : boolean) : void { 122 this.isDestroy_ = isDestroy; 123 if (this.isDestroy_) { 124 this.rootNode = null; 125 } 126 } 127 128 postEvent(event: TouchEvent | undefined) : boolean { 129 return this.rootNode?.postTouchEvent(event) as boolean 130 } 131 } 132 133 @Component 134 struct VideoComponent { 135 @ObjectLink params: Params 136 @State bkColor: Color = Color.Red 137 mXComponentController: XComponentController = new XComponentController(); 138 @State player_changed: boolean = false; 139 player?: AVPlayerDemo; 140 141 build() { 142 Column() { 143 Button(this.params.textOne) 144 145 XComponent({ id: 'video_player_id', type: XComponentType.SURFACE, controller: this.mXComponentController}) 146 .border({width: 1, color: Color.Red}) 147 .onLoad(() => { 148 this.player = new AVPlayerDemo(); 149 this.player.setSurfaceID(this.mXComponentController.getXComponentSurfaceId()); 150 this.player_changed = !this.player_changed; 151 this.player.avPlayerLiveDemo() 152 }) 153 .width(300) 154 .height(200) 155 } 156 // The width and height of the outermost container in the custom component must be the width and height of the tag at the same layer. 157 .width(this.params.width) 158 .height(this.params.height) 159 } 160 } 161 // In @Builder, add the specific dynamic component content. 162 @Builder 163 function VideoBuilder(params: Params) { 164 VideoComponent({ params: params }) 165 .backgroundColor(Color.Gray) 166 } 167 168 @Entry 169 @Component 170 struct WebIndex { 171 browserTabController: WebviewController = new webview.WebviewController() 172 private nodeControllerMap: Map<string, MyNodeController> = new Map(); 173 @State componentIdArr: Array<string> = []; 174 175 aboutToAppear() { 176 // Enable web frontend page debugging. 177 webview.WebviewController.setWebDebuggingAccess(true); 178 } 179 180 build(){ 181 Row() { 182 Column() { 183 Stack() { 184 ForEach(this.componentIdArr, (componentId: string) => { 185 NodeContainer(this.nodeControllerMap.get(componentId)) 186 }, (embedId: string) => embedId) 187 // Load the local test.html page. 188 Web({ src: $rawfile("test.html"), controller: this.browserTabController }) 189 // Enable same-layer rendering. 190 .enableNativeEmbedMode(true) 191 // Obtain the lifecycle change data of the embed element. 192 .onNativeEmbedLifecycleChange((embed) => { 193 console.log("NativeEmbed surfaceId" + embed.surfaceId); 194 // 1. If embed.info.id is used as the key for mapping nodeController, explicitly specify the ID on the HTML5 page. 195 const componentId = embed.info?.id?.toString() as string 196 if (embed.status == NativeEmbedStatus.CREATE) { 197 console.log("NativeEmbed create" + JSON.stringify(embed.info)) 198 // Create a NodeController instance, set parameters, and rebuild. 199 let nodeController = new MyNodeController() 200 // 1. The unit of embed.info.width and embed.info.height is px, which needs to be converted to the default unit vp on the ets side. 201 nodeController.setRenderOption({surfaceId : embed.surfaceId as string, type : embed.info?.type as string, 202 renderType : NodeRenderType.RENDER_TYPE_TEXTURE, embedId : embed.embedId as string, 203 width : px2vp(embed.info?.width), height : px2vp(embed.info?.height)}) 204 nodeController.setDestroy(false); 205 // Save the NodeController instance to the map, with the ID attribute of the embed element passed in by the Web component as the key. 206 this.nodeControllerMap.set(componentId, nodeController) 207 // Save the ID attribute of the embed element passed in by the Web component to the @State decorated array variable for creating a node container dynamically. The push action must be executed after the set action. 208 this.componentIdArr.push(componentId) 209 } else if (embed.status == NativeEmbedStatus.UPDATE) { 210 let nodeController = this.nodeControllerMap.get(componentId) 211 nodeController?.updateNode({textOne: 'update', width: px2vp(embed.info?.width), height: px2vp(embed.info?.height)} as ESObject) 212 } else { 213 let nodeController = this.nodeControllerMap.get(componentId); 214 nodeController?.setDestroy(true) 215 this.nodeControllerMap.clear(); 216 this.componentIdArr.length = 0; 217 } 218 })// Obtain the touch event information of components for same-layer rendering. 219 .onNativeEmbedGestureEvent((touch) => { 220 console.log("NativeEmbed onNativeEmbedGestureEvent" + JSON.stringify(touch.touchEvent)); 221 this.componentIdArr.forEach((componentId: string) => { 222 let nodeController = this.nodeControllerMap.get(componentId) 223 // Send the obtained event of the region at the same layer to the nodeController corresponding to embedId of the region. 224 if (nodeController?.getEmbedId() === touch.embedId) { 225 let ret = nodeController?.postEvent(touch.touchEvent) 226 if (ret) { 227 console.log("onNativeEmbedGestureEvent success " + componentId) 228 } else { 229 console.log("onNativeEmbedGestureEvent fail " + componentId) 230 } 231 if (touch.result) { 232 // Notify the <Web> component of the gesture event consumption result. 233 touch.result.setGestureEventResult(ret); 234 } 235 } 236 }) 237 }) 238 } 239 } 240 } 241 } 242 } 243 ``` 244 245- Example of using video playback on the application side: 246 247 ```ts 248 // HAP's src/main/ets/pages/PlayerDemo.ets 249 import { media } from '@kit.MediaKit'; 250 import { BusinessError } from '@ohos.base'; 251 252 export class AVPlayerDemo { 253 private count: number = 0; 254 private surfaceID: string = ''; // The surfaceID parameter specifies the window used to display the video. Its value is obtained through XComponent. 255 private isSeek: boolean = true; // Specify whether the seek operation is supported. 256 257 setSurfaceID(surface_id: string){ 258 console.log('setSurfaceID : ' + surface_id); 259 this.surfaceID = surface_id; 260 } 261 // Set AVPlayer callback functions. 262 setAVPlayerCallback(avPlayer: media.AVPlayer) { 263 // Callback function for the seek operation. 264 avPlayer.on('seekDone', (seekDoneTime: number) => { 265 console.info(`AVPlayer seek succeeded, seek time is ${seekDoneTime}`); 266 }) 267 // Callback function for errors. If an error occurs during the operation on the AVPlayer, reset() is called to reset the AVPlayer. 268 avPlayer.on('error', (err: BusinessError) => { 269 console.error(`Invoke avPlayer failed, code is ${err.code}, message is ${err.message}`); 270 avPlayer.reset(); 271 }) 272 // Callback function for state changes. 273 avPlayer.on('stateChange', async (state: string, reason: media.StateChangeReason) => { 274 switch (state) { 275 case 'idle': // This state is reported upon a successful callback of reset(). 276 console.info('AVPlayer state idle called.'); 277 avPlayer.release(); // Call release() to release the instance. 278 break; 279 case 'initialized': // This state is reported when the AVPlayer sets the playback source. 280 console.info('AVPlayer state initialized called.'); 281 avPlayer.surfaceId = this.surfaceID; // Set the window to display the video. This setting is not required when a pure audio asset is to be played. 282 avPlayer.prepare(); 283 break; 284 case 'prepared': // This state is reported upon a successful callback of prepare(). 285 console.info('AVPlayer state prepared called.'); 286 avPlayer.play(); // Call play() to start playback. 287 break; 288 case 'playing': // This state is reported upon a successful callback of play(). 289 console.info('AVPlayer state prepared called.'); 290 if(this.count !== 0) { 291 if (this.isSeek) { 292 console.info('AVPlayer start to seek.'); 293 avPlayer.seek(avPlayer.duration); // Call seek() to seek to the end of the video clip. 294 } else { 295 // When the seek operation is not supported, the playback continues until it reaches the end. 296 console.info('AVPlayer wait to play end.'); 297 } 298 } else { 299 avPlayer.pause(); // Call pause() to pause the playback. 300 } 301 this.count++; 302 break; 303 case 'paused': // This state is reported upon a successful callback of pause(). 304 console.info('AVPlayer state paused called.'); 305 avPlayer.play(); // Call play() again to start playback. 306 break; 307 case 'completed': // This state is reported upon the completion of the playback. 308 console.info('AVPlayer state paused called.'); 309 avPlayer.stop(); // Call stop() to stop the playback. 310 break; 311 case 'stopped': // This state is reported upon a successful callback of stop(). 312 console.info('AVPlayer state stopped called.'); 313 avPlayer.reset(); // Call reset() to reset the AVPlayer. 314 break; 315 case 'released': // This state is reported upon the release of the AVPlayer. 316 console.info('AVPlayer state released called.'); 317 break; 318 default: 319 break; 320 } 321 }) 322 } 323 324 // Set the live stream source through the URL. 325 async avPlayerLiveDemo(){ 326 // Create an AVPlayer instance. 327 let avPlayer: media.AVPlayer = await media.createAVPlayer(); 328 // Set a callback function for state changes. 329 this.setAVPlayerCallback(avPlayer); 330 this.isSeek = false; // The seek operation is not supported. 331 // Replace the URL with the actual URL of the video source. 332 avPlayer.url = 'https://xxx.xxx/demo.mp4'; 333 } 334 } 335 ``` 336 337- Example of the frontend page: 338 339 ```html 340 <!--HAP's src/main/resources/rawfile/test.html--> 341 <!DOCTYPE html> 342 <html> 343 <head> 344 <title>Same-layer rendering test html</title> 345 <meta name="viewport"> 346 </head> 347 <body> 348 <div> 349 <div id="bodyId"> 350 <embed id="nativeVideo" type = "native/video" width="1000" height="1500" src="test" style = "background-color:red"/> 351 </div> 352 </div> 353 </body> 354 </html> 355 ``` 356 357  358 359### Enabling Same-Layer Rendering and Specifying the Label Name and Custom Type 360 361You can also use [registerNativeEmbedRule(tag: string, type: string)](../reference/apis-arkweb/ts-basic-components-web.md#registernativeembedrule12) to specify the tag and type. 362 363For the **tag** parameter, only **embed** and **object** are supported. For the **type** parameter, you can specify any string. These two parameters are case insensitive: The ArkWeb kernel converts the values into lowercase letters. The **tag** parameter uses the full string for matching, and **type** uses the prefix for matching. 364 365If you do not use this API or the API receives an invalid string (for example, an empty string), the kernel uses the default prefix mode "embed" + "native/". If the specified type is the same as any object or embedded type defined by W3C, as in **registerNativeEmbedRule("object", "application/pdf")**, 366ArkWeb will follow the W3C standard behavior and will not identify it as a tag at the same layer. 367 368- Example of using **registerNativeEmbedRule** on the application side: 369 370 ```ts 371 class MyNodeController extends NodeController { 372 ... 373 makeNode(uiContext: UIContext): FrameNode | null{ 374 375 if (this.type_ === 'test') { 376 ... 377 } else if (this.type_ === 'test/video') { 378 ... 379 } else { 380 // other 381 } 382 ... 383 } 384 ... 385 } 386 ... 387 388 build(){ 389 ... 390 Stack() { 391 ... 392 Web({ src: $rawfile("test.html"), controller: this.browserTabController }) 393 // Enable same-layer rendering. 394 .enableNativeEmbedMode(true) 395 // Register the same-layer tag of "object" and type of "test." 396 .registerNativeEmbedRule("object", "test") 397 ... 398 } 399 ... 400 } 401 402 ``` 403 404- Example of using **registerNativeEmbedRule** on the frontend page, with the tag of "object" and type of "test": 405 406 ```html 407 408 <!DOCTYPE html> 409 <html> 410 <head> 411 <title>Same-layer rendering test html</title> 412 <meta name="viewport"> 413 </head> 414 <body> 415 <div> 416 <div> 417 <object id="nativeButton" type="test" width="800" height="800" data="test?params1=xxx?" style = "background-color:red"/> 418 <param name="id" value="playerId" /> 419 <param name="data" value='{}' /> 420 </object> 421 </div> 422 <div> 423 <object id="nativeVideo" type="test/video" width="500" height="500" data="test" style = "background-color:red"/><object> 424 </div> 425 </div> 426 <div id="button" width="500" height="200"> 427 <p>bottom</p> 428 </div> 429 430 </body> 431 </html> 432 ``` 433 434## Drawing the TextInput Component and Synchronizing Position Information Returned During Same-Layer Element Updates to the Component 435 436The same-layer elements are updated as a result of scrolling, scaling, or any other behavior that may cause a re-layout. The positions of same-layer elements are based on the **\<Web>** component coordinate system. For web page scaling that does not change the element size, only the position changes, and the width and height remain at the initial values. 437 438For components that require location information, such as **\<TextInput>** and **\<TextArea>**, you need to synchronize the location information reported by the same-layer elements to the components in real time. 439 440- Complete sample code on the application side: 441 442 ```ts 443 ... 444 class MyNodeController extends NodeController { 445 ... 446 makeNode(uiContext: UIContext): FrameNode | null{ 447 448 if (this.type_ === 'application/view') { 449 this.rootNode.build(wrapBuilder(TextInputBuilder), { 450 textOne: "myInput", 451 width: this.width_, 452 height: this.height_ 453 }); 454 } else { 455 // other 456 } 457 ... 458 } 459 ... 460 } 461 462 463 @Component 464 struct TextInputComponent { 465 @Prop params: Params 466 @State bkColor: Color = Color.Red 467 mXComponentController: XComponentController = new XComponentController(); 468 469 build() { 470 Column() { 471 TextInput({ text: `${this.params.textOne}` }) 472 .height(50) 473 .width(200) 474 .backgroundColor(Color.Green) 475 .onTouch((event) => { 476 console.log('input1 event ' + JSON.stringify(event)); 477 }).margin({ top: 30}) 478 479 TextInput({ text: `${this.params.textOne}` }) 480 .height(50) 481 .width(200) 482 .backgroundColor(Color.Green) 483 .onTouch((event) => { 484 console.log('input2 event ' + JSON.stringify(event)); 485 }).margin({ top: 30}) 486 487 TextInput({ text: `${this.params.textOne}` }) 488 .height(50) 489 .width(200) 490 .backgroundColor(Color.Green) 491 .onTouch((event) => { 492 console.log('input2 event ' + JSON.stringify(event)); 493 }).margin({ top: 30}) 494 } 495 .width(this.params.width) 496 .height(this.params.height) 497 } 498 } 499 500 @Builder 501 function TextInputBuilder(params: Params) { 502 TextInputComponent({ params: params }) 503 .height(params.height) 504 .width(params.width) 505 .backgroundColor(Color.Red) 506 } 507 508 @Entry 509 @Component 510 struct Page { 511 browserTabController: WebviewController = new webview.WebviewController() 512 private nodeControllerMap: Map<string, MyNodeController> = new Map(); 513 @State componentIdArr: Array<string> = []; 514 @State edges: Edges = {}; 515 516 build() { 517 Row() { 518 Column() { 519 Stack(){ 520 ForEach(this.componentIdArr, (componentId: string) => { 521 NodeContainer(this.nodeControllerMap.get(componentId)).position(this.edges) 522 }, (embedId: string) => embedId) 523 524 Web({ src: $rawfile('test.html'), controller: this.browserTabController}) 525 .enableNativeEmbedMode(true) 526 .registerNativeEmbedRule("object", "APPlication/view") 527 .onNativeEmbedLifecycleChange((embed) => { 528 const componentId = embed.info?.id?.toString() as string; 529 if (embed.status == NativeEmbedStatus.CREATE) { 530 // You are advised to use position in edges mode to avoid extra precision loss caused by floating-point calculation during the conversion between px and vp. 531 this.edges = {left: `${embed.info?.position?.x as number}px`, top: `${embed.info?.position?.y as number}px`} 532 let nodeController = new MyNodeController() 533 nodeController.setRenderOption({surfaceId : embed.surfaceId as string, 534 type : embed.info?.type as string, 535 renderType : NodeRenderType.RENDER_TYPE_TEXTURE, 536 embedId : embed.embedId as string, 537 width : px2vp(embed.info?.width), 538 height :px2vp(embed.info?.height)}) 539 nodeController.rebuild() 540 541 this.nodeControllerMap.set(componentId, nodeController) 542 this.componentIdArr.push(componentId) 543 } else if (embed.status == NativeEmbedStatus.UPDATE) { 544 console.log("NativeEmbed update" + JSON.stringify(embed.info)) 545 546 this.edges = {left: `${embed.info?.position?.x as number}px`, top: `${embed.info?.position?.y as number}px`} 547 let nodeController = this.nodeControllerMap.get(componentId) 548 549 nodeController?.updateNode({text: 'update', width : px2vp(embed.info?.width), 550 height :px2vp(embed.info?.height)} as ESObject) 551 nodeController?.rebuild() 552 } else { 553 let nodeController = this.nodeControllerMap.get(componentId) 554 nodeController?.setBuilderNode(null) 555 nodeController?.rebuild() 556 } 557 }) 558 .onNativeEmbedGestureEvent((touch) => { 559 this.componentIdArr.forEach((componentId: string) => { 560 let nodeController = this.nodeControllerMap.get(componentId) 561 if (nodeController?.getEmbedId() === touch.embedId) { 562 let ret = nodeController?.postEvent(touch.touchEvent) 563 if (ret) { 564 console.log("onNativeEmbedGestureEvent success " + componentId) 565 } else { 566 console.log("onNativeEmbedGestureEvent fail " + componentId) 567 } 568 } 569 }) 570 }) 571 } 572 } 573 .width('100%') 574 } 575 .height('100%') 576 } 577 } 578 579 ``` 580 581- Example of the frontend page: 582 583 ```html 584 <!DOCTYPE html> 585 <html> 586 <head> 587 <title>Same-layer rendering test html</title> 588 <meta charset="UTF-8"> 589 <style> 590 html { 591 background-color: blue; 592 } 593 </style> 594 </head> 595 <body> 596 597 <div id="bodyId" style="width:800px; height:1000px; margin-top:1000px;"> 598 <object id="cameraTest" type="application/view" width="100%" height="100%" ></object> 599 </div> 600 <div style="height:1000px;"> 601 </div> 602 603 </body> 604 </html> 605 ``` 606