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  ![web-same-layer](figures/web-same-layer.png)
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