• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# 托管网页中的媒体播放
2
3Web组件提供了应用接管网页中媒体播放的能力,用来支持应用增强网页的媒体播放,如画质增强等。
4
5## 使用场景
6
7网页播放媒体时,存在着网页视频不够清晰、网页的播放器界面简陋功能少、一些视频不能播放的问题。
8
9此时,应用开发者可以使用该功能,通过自己或者第三方的播放器,接管网页媒体播放来改善网页的媒体播放体验。
10
11## 实现原理
12
13### ArkWeb内核播放媒体的框架
14
15不开启该功能时,ArkWeb内核的播放架构如下所示:
16
17  ![arkweb media pipeline](figures/arkweb_media_pipeline.png)
18
19  > **说明:**
20  >
21  > - 上图中1表示ArkWeb内核创建WebMediaPlayer来播放网页中的媒体资源。
22  > - 上图中2表示WebMediaPlayer使用系统解码器来渲染媒体数据。
23
24开启该功能后,ArkWeb内核的播放架构如下:
25
26  ![arkweb native media player](figures/arkweb_native_media_player.png)
27
28  > **说明:**
29  >
30  > - 上图中1表示ArkWeb内核创建WebMediaPlayer来播放网页中的媒体资源。
31  > - 上图中2表示WebMediaPlayer使用应用提供的本地播放器(NativeMediaPlayer)来渲染媒体数据。
32
33
34### ArkWeb内核与应用的交互
35
36  ![interactions between arkweb and native media player](figures/interactions_between_arkweb_and_native_media_player.png)
37
38  > **说明:**
39  >
40  > - 上图中1的详细说明见[开启接管网页媒体播放](#开启接管网页媒体播放)。
41  > - 上图中2的详细说明见[创建本地播放器](#创建本地播放器nativemediaplayer)。
42  > - 上图中3的详细说明见[绘制本地播放器组件](#绘制本地播放器组件)。
43  > - 上图中4的详细说明见[执行 ArkWeb 内核发送给本地播放器的播控指令](#执行arkweb内核发送给本地播放器的播控指令)。
44  > - 上图中5的详细说明见[将本地播放器的状态信息通知给 ArkWeb 内核](#将本地播放器的状态信息通知给arkweb内核)。
45
46## 开发指导
47
48### 开启接管网页媒体播放
49
50需要先通过[enableNativeMediaPlayer](../reference/apis-arkweb/ts-basic-components-web.md#enablenativemediaplayer12)接口,开启接管网页媒体播放的功能。
51
52  ```ts
53  // xxx.ets
54  import { webview } from '@kit.ArkWeb';
55
56  @Entry
57  @Component
58  struct WebComponent {
59    controller: webview.WebviewController = new webview.WebviewController();
60
61    build() {
62      Column() {
63        Web({ src: 'www.example.com', controller: this.controller })
64          .enableNativeMediaPlayer({ enable: true, shouldOverlay: false })
65      }
66    }
67  }
68  ```
69
70### 创建本地播放器(NativeMediaPlayer)
71
72该功能开启后,每当网页中有媒体需要播放时,ArkWeb内核会触发由[onCreateNativeMediaPlayer](../reference/apis-arkweb/js-apis-webview.md#oncreatenativemediaplayer12)注册的回调函数。
73
74开发者则需要调用 `onCreateNativeMediaPlayer` 来注册一个创建本地播放器的回调函数。
75
76该回调函数需要根据媒体信息来判断是否要创建一个本地播放器来接管当前的网页媒体资源。
77
78  * 如果应用不接管当前的为网页媒体资源, 需要在回调函数里返回 `null` 。
79  * 如果应用接管当前的为网页媒体资源, 需要在回调函数里返回一个本地播放器实例。
80
81本地播放器需要实现[NativeMediaPlayerBridge](../reference/apis-arkweb/js-apis-webview.md#nativemediaplayerbridge12)接口,以便ArkWeb内核对本地播放器进行播控操作。
82
83  ```ts
84  // xxx.ets
85  import { webview } from '@kit.ArkWeb';
86
87  // 实现 webview.NativeMediaPlayerBridge 接口。
88  // ArkWeb 内核调用该类的方法来对 NativeMediaPlayer 进行播控。
89  class NativeMediaPlayerImpl implements webview.NativeMediaPlayerBridge {
90    // ... 实现 NativeMediaPlayerBridge 里的接口方法 ...
91    constructor(handler: webview.NativeMediaPlayerHandler, mediaInfo: webview.MediaInfo) {}
92    updateRect(x: number, y: number, width: number, height: number) {}
93    play() {}
94    pause() {}
95    seek(targetTime: number) {}
96    release() {}
97    setVolume(volume: number) {}
98    setMuted(muted: boolean) {}
99    setPlaybackRate(playbackRate: number) {}
100    enterFullscreen() {}
101    exitFullscreen() {}
102  }
103
104  @Entry
105  @Component
106  struct WebComponent {
107    controller: webview.WebviewController = new webview.WebviewController();
108
109    build() {
110      Column() {
111        Web({ src: 'www.example.com', controller: this.controller })
112          .enableNativeMediaPlayer({ enable: true, shouldOverlay: false })
113          .onPageBegin((event) => {
114            this.controller.onCreateNativeMediaPlayer((handler: webview.NativeMediaPlayerHandler, mediaInfo: webview.MediaInfo) => {
115              // 判断需不需要接管当前的媒体。
116              if (!shouldHandle(mediaInfo)) {
117                // 本地播放器不接管该媒体。
118                // 返回 null 。ArkWeb 内核将用自己的播放器来播放该媒体。
119                return null;
120              }
121              // 接管当前的媒体。
122              // 返回一个本地播放器实例给 ArkWeb 内核。
123              let nativePlayer: webview.NativeMediaPlayerBridge = new NativeMediaPlayerImpl(handler, mediaInfo);
124              return nativePlayer;
125            });
126          })
127      }
128    }
129  }
130
131  // stub
132  function shouldHandle(mediaInfo: webview.MediaInfo) {
133    return true;
134  }
135  ```
136
137### 绘制本地播放器组件
138
139应用接管网页的媒体后,开发者需要将本地播放器组件及视频画面绘制到ArkWeb内核提供的Surface上。ArkWeb内核再将Surface与网页进行合成,最后上屏显示。
140
141该流程与[同层渲染绘制](web-same-layer.md)相同。
142
1431. 在应用启动阶段,应保存UIContext,以便后续的同层渲染绘制流程能够使用该UIContext。
144
145   ```ts
146   // xxxAbility.ets
147
148   import { UIAbility } from '@kit.AbilityKit';
149   import { window } from '@kit.ArkUI';
150
151   export default class EntryAbility extends UIAbility {
152     onWindowStageCreate(windowStage: window.WindowStage): void {
153       windowStage.loadContent('pages/Index', (err, data) => {
154         if (err.code) {
155           return;
156         }
157         // 保存UIContext, 在后续的同层渲染绘制中使用。
158         AppStorage.setOrCreate<UIContext>("UIContext", windowStage.getMainWindowSync().getUIContext());
159       });
160     }
161
162     // ... 其他需要重写的方法 ...
163   }
164   ```
165
1662. 使用ArkWeb内核创建的Surface进行同层渲染绘制。
167
168   ```ts
169   // xxx.ets
170   import { webview } from '@kit.ArkWeb';
171   import { BuilderNode, FrameNode, NodeController, NodeRenderType } from '@kit.ArkUI';
172
173   interface ComponentParams {}
174
175   class MyNodeController extends NodeController {
176     private rootNode: BuilderNode<[ComponentParams]> | undefined;
177
178     constructor(surfaceId: string, renderType: NodeRenderType) {
179       super();
180
181       // 获取之前保存的 UIContext 。
182       let uiContext = AppStorage.get<UIContext>("UIContext");
183       this.rootNode = new BuilderNode(uiContext as UIContext, { surfaceId: surfaceId, type: renderType });
184     }
185
186     makeNode(uiContext: UIContext): FrameNode | null {
187       if (this.rootNode) {
188         return this.rootNode.getFrameNode() as FrameNode;
189       }
190       return null;
191     }
192
193     build() {
194       // 构造本地播放器组件
195     }
196   }
197
198   @Entry
199   @Component
200   struct WebComponent {
201     node_controller?: MyNodeController;
202     controller: webview.WebviewController = new webview.WebviewController();
203     @State show_native_media_player: boolean = false;
204
205     build() {
206       Column() {
207         Stack({ alignContent: Alignment.TopStart }) {
208           if (this.show_native_media_player) {
209             NodeContainer(this.node_controller)
210               .width(300)
211               .height(150)
212               .backgroundColor(Color.Transparent)
213               .border({ width: 2, color: Color.Orange })
214           }
215           Web({ src: 'www.example.com', controller: this.controller })
216             .enableNativeMediaPlayer({ enable: true, shouldOverlay: false })
217             .onPageBegin((event) => {
218               this.controller.onCreateNativeMediaPlayer((handler: webview.NativeMediaPlayerHandler, mediaInfo:    webview.MediaInfo) => {
219                 // 接管当前的媒体。
220
221                 // 使用同层渲染流程提供的 surface 来构造一个本地播放器组件。
222                 this.node_controller = new MyNodeController(mediaInfo.surfaceInfo.id, NodeRenderType.RENDER_TYPE_TEXTURE);
223                 this.node_controller.build();
224
225                 // 展示本地播放器组件。
226                 this.show_native_media_player = true;
227
228                 // 返回一个本地播放器实例给 ArkWeb 内核。
229                 return null;
230               });
231             })
232         }
233       }
234     }
235   }
236   ```
237
238动态创建组件并绘制到Surface上的详细介绍见[同层渲染绘制](web-same-layer.md) 。
239
240### 执行ArkWeb内核发送给本地播放器的播控指令
241
242为了方便ArkWeb内核对本地播放器进行播控操作,应用开发者需要令本地播放器实现[NativeMediaPlayerBridge](../reference/apis-arkweb/js-apis-webview.md#nativemediaplayerbridge12)接口,并根据每个接口方法的功能对本地播放器进行相应操作。
243
244  ```ts
245  // xxx.ets
246  import { webview } from '@kit.ArkWeb';
247
248  class ActualNativeMediaPlayerListener {
249    constructor(handler: webview.NativeMediaPlayerHandler) {}
250  }
251
252  class NativeMediaPlayerImpl implements webview.NativeMediaPlayerBridge {
253    constructor(handler: webview.NativeMediaPlayerHandler, mediaInfo: webview.MediaInfo) {
254      // 1. 创建一个本地播放器的状态监听。
255      let listener: ActualNativeMediaPlayerListener = new ActualNativeMediaPlayerListener(handler);
256      // 2. 创建一个本地播放器。
257      // 3. 监听该本地播放器。
258      // ...
259    }
260
261    updateRect(x: number, y: number, width: number, height: number) {
262      // <video> 标签的位置和大小发生了变化。
263      // 根据该信息变化,作出相应的改变。
264    }
265
266    play() {
267      // 启动本地播放器播放。
268    }
269
270    pause() {
271      // 暂停本地播放器播放。
272    }
273
274    seek(targetTime: number) {
275      // 本地播放器跳转到指定的时间点。
276    }
277
278    release() {
279      // 销毁本地播放器。
280    }
281
282    setVolume(volume: number) {
283      // ArkWeb 内核要求调整本地播放器的音量。
284      // 设置本地播放器的音量。
285    }
286
287    setMuted(muted: boolean) {
288      // 将本地播放器静音或取消静音。
289    }
290
291    setPlaybackRate(playbackRate: number) {
292      // 调整本地播放器的播放速度。
293    }
294
295    enterFullscreen() {
296      // 将本地播放器设置为全屏播放。
297    }
298
299    exitFullscreen() {
300      // 将本地播放器退出全屏播放。
301    }
302  }
303  ```
304
305### 将本地播放器的状态信息通知给ArkWeb内核
306
307ArkWeb内核需要本地播放器的状态信息来更新到网页(例如:视频的宽高、播放时间、缓存时间等),因此,应用开发者需要将本地播放器的状态信息通知给ArkWeb内核。
308
309在[onCreateNativeMediaPlayer](../reference/apis-arkweb/js-apis-webview.md#oncreatenativemediaplayer12)接口中, ArkWeb内核传递给应用一个[NativeMediaPlayerHandler](../reference/apis-arkweb/js-apis-webview.md#nativemediaplayerhandler12)对象。应用开发者需要通过该对象,将本地播放器的最新状态信息通知给ArkWeb内核。
310
311  ```ts
312  // xxx.ets
313  import { webview } from '@kit.ArkWeb';
314
315  class ActualNativeMediaPlayerListener {
316    handler: webview.NativeMediaPlayerHandler;
317
318    constructor(handler: webview.NativeMediaPlayerHandler) {
319      this.handler = handler;
320    }
321
322    onPlaying() {
323      // 本地播放器开始播放。
324      this.handler.handleStatusChanged(webview.PlaybackStatus.PLAYING);
325    }
326    onPaused() {
327      // 本地播放器暂停播放。
328      this.handler.handleStatusChanged(webview.PlaybackStatus.PAUSED);
329    }
330    onSeeking() {
331      // 本地播放器开始执行跳转到目标时间点。
332      this.handler.handleSeeking();
333    }
334    onSeekDone() {
335      // 本地播放器 seek 完成。
336      this.handler.handleSeekFinished();
337    }
338    onEnded() {
339      // 本地播放器播放完成。
340      this.handler.handleEnded();
341    }
342    onVolumeChanged() {
343      // 获取本地播放器的音量。
344      let volume: number = getVolume();
345      this.handler.handleVolumeChanged(volume);
346    }
347    onCurrentPlayingTimeUpdate() {
348      // 更新播放时间。
349      let currentTime: number = getCurrentPlayingTime();
350      // 将时间单位换算成秒。
351      let currentTimeInSeconds = convertToSeconds(currentTime);
352      this.handler.handleTimeUpdate(currentTimeInSeconds);
353    }
354    onBufferedChanged() {
355      // 缓存发生了变化。
356      // 获取本地播放器的缓存时长。
357      let bufferedEndTime: number = getCurrentBufferedTime();
358      // 将时间单位换算成秒。
359      let bufferedEndTimeInSeconds = convertToSeconds(bufferedEndTime);
360      this.handler.handleBufferedEndTimeChanged(bufferedEndTimeInSeconds);
361
362      // 检查缓存状态。
363      // 如果缓存状态发生了变化,则向 ArkWeb 内核通知缓存状态。
364      let lastReadyState: webview.ReadyState = getLastReadyState();
365      let currentReadyState: webview.ReadyState = getCurrentReadyState();
366      if (lastReadyState != currentReadyState) {
367        this.handler.handleReadyStateChanged(currentReadyState);
368      }
369    }
370    onEnterFullscreen() {
371      // 本地播放器进入了全屏状态。
372      let isFullscreen: boolean = true;
373      this.handler.handleFullscreenChanged(isFullscreen);
374    }
375    onExitFullscreen() {
376      // 本地播放器退出了全屏状态。
377      let isFullscreen: boolean = false;
378      this.handler.handleFullscreenChanged(isFullscreen);
379    }
380    onUpdateVideoSize(width: number, height: number) {
381      // 当本地播放器解析出视频宽高时, 通知 ArkWeb 内核。
382      this.handler.handleVideoSizeChanged(width, height);
383    }
384    onDurationChanged(duration: number) {
385      // 本地播放器解析到了新的媒体时长, 通知 ArkWeb 内核。
386      this.handler.handleDurationChanged(duration);
387    }
388    onError(error: webview.MediaError, errorMessage: string) {
389      // 本地播放器出错了,通知 ArkWeb 内核。
390      this.handler.handleError(error, errorMessage);
391    }
392    onNetworkStateChanged(state: webview.NetworkState) {
393      // 本地播放器的网络状态发生了变化, 通知 ArkWeb 内核。
394      this.handler.handleNetworkStateChanged(state);
395    }
396    onPlaybackRateChanged(playbackRate: number) {
397      // 本地播放器的播放速率发生了变化, 通知 ArkWeb 内核。
398      this.handler.handlePlaybackRateChanged(playbackRate);
399    }
400    onMutedChanged(muted: boolean) {
401      // 本地播放器的静音状态发生了变化, 通知 ArkWeb 内核。
402      this.handler.handleMutedChanged(muted);
403    }
404
405    // ... 监听本地播放器其他的状态 ...
406  }
407  @Entry
408  @Component
409  struct WebComponent {
410    controller: webview.WebviewController = new webview.WebviewController();
411    @State show_native_media_player: boolean = false;
412
413    build() {
414      Column() {
415        Web({ src: 'www.example.com', controller: this.controller })
416          .enableNativeMediaPlayer({enable: true, shouldOverlay: false})
417          .onPageBegin((event) => {
418            this.controller.onCreateNativeMediaPlayer((handler: webview.NativeMediaPlayerHandler, mediaInfo: webview.MediaInfo) => {
419              // 接管当前的媒体。
420
421              // 创建一个本地播放器实例。
422              // let nativePlayer: NativeMediaPlayerImpl = new NativeMediaPlayerImpl(handler, mediaInfo);
423
424              // 创建一个本地播放器状态监听对象。
425              let nativeMediaPlayerListener: ActualNativeMediaPlayerListener = new ActualNativeMediaPlayerListener(handler);
426              // 监听本地播放器状态。
427              // nativePlayer.setListener(nativeMediaPlayerListener);
428
429              // 返回这个本地播放器实例给 ArkWeb 内核。
430              return null;
431            });
432          })
433      }
434    }
435  }
436
437  // stub
438  function getVolume() {
439    return 1;
440  }
441  function getCurrentPlayingTime() {
442    return 1;
443  }
444  function getCurrentBufferedTime() {
445    return 1;
446  }
447  function convertToSeconds(input: number) {
448    return input;
449  }
450  function getLastReadyState() {
451    return webview.ReadyState.HAVE_NOTHING;
452  }
453  function getCurrentReadyState() {
454    return webview.ReadyState.HAVE_NOTHING;
455  }
456  ```
457
458
459## 完整示例
460
461- 使用前请在module.json5添加如下权限。
462
463  ```ts
464  "ohos.permission.INTERNET"
465  ```
466
467- 应用侧代码,在应用启动阶段保存UIContext。
468
469  ```ts
470  // xxxAbility.ets
471
472  import { UIAbility } from '@kit.AbilityKit';
473  import { window } from '@kit.ArkUI';
474
475  export default class EntryAbility extends UIAbility {
476    onWindowStageCreate(windowStage: window.WindowStage): void {
477      windowStage.loadContent('pages/Index', (err, data) => {
478        if (err.code) {
479          return;
480        }
481        // 保存 UIContext, 在后续的同层渲染绘制中会用到。
482        AppStorage.setOrCreate<UIContext>("UIContext", windowStage.getMainWindowSync().getUIContext());
483      });
484    }
485
486    // ... 其他需要重写的方法 ...
487  }
488  ```
489
490- 应用侧代码,视频托管使用示例。
491
492  ```ts
493  // Index.ets
494  import { webview } from '@kit.ArkWeb';
495  import { BuilderNode, FrameNode, NodeController, NodeRenderType } from '@kit.ArkUI';
496  import { AVPlayerDemo, AVPlayerListener } from './PlayerDemo';
497
498  // 实现 webview.NativeMediaPlayerBridge 接口。
499  // ArkWeb 内核调用该类的方法来对 NativeMediaPlayer 进行播控。
500  class NativeMediaPlayerImpl implements webview.NativeMediaPlayerBridge {
501    private surfaceId: string;
502    mediaSource: string;
503    private mediaHandler: webview.NativeMediaPlayerHandler;
504    nativePlayerInfo: NativePlayerInfo;
505    nativePlayer: AVPlayerDemo;
506    httpHeaders: Record<string, string>;
507    uiContext?: UIContext;
508
509    constructor(nativePlayerInfo: NativePlayerInfo, handler: webview.NativeMediaPlayerHandler, mediaInfo: webview.MediaInfo, uiContext: UIContext) {
510      this.uiContext = uiContext;
511      console.log(`NativeMediaPlayerImpl.constructor, surface_id[${mediaInfo.surfaceInfo.id}]`);
512      this.nativePlayerInfo = nativePlayerInfo;
513      this.mediaHandler = handler;
514      this.surfaceId = mediaInfo.surfaceInfo.id;
515      this.mediaSource = mediaInfo.mediaSrcList.find((item)=>{item.source.indexOf('.mp4') > 0})?.source
516        || mediaInfo.mediaSrcList[0].source;
517      this.httpHeaders = mediaInfo.headers;
518      this.nativePlayer = new AVPlayerDemo();
519
520      // 使用同层渲染功能,将视频及其播控组件绘制到网页中
521      this.nativePlayerInfo.node_controller = new MyNodeController(
522        this.nativePlayerInfo, this.surfaceId, this.mediaHandler, this, NodeRenderType.RENDER_TYPE_TEXTURE);
523      this.nativePlayerInfo.node_controller.build();
524      this.nativePlayerInfo.show_native_media_player = true;
525
526      console.log(`NativeMediaPlayerImpl.mediaSource: ${this.mediaSource}, headers: ${JSON.stringify(this.httpHeaders)}`);
527    }
528
529    updateRect(x: number, y: number, width: number, height: number): void {
530      let width_in_vp = this.uiContext!.px2vp(width);
531      let height_in_vp = this.uiContext!.px2vp(height);
532      console.log(`updateRect(${x}, ${y}, ${width}, ${height}), vp:{${width_in_vp}, ${height_in_vp}}`);
533
534      this.nativePlayerInfo.updateNativePlayerRect(x, y, width, height);
535    }
536
537    play() {
538      console.log('NativeMediaPlayerImpl.play');
539      this.nativePlayer.play();
540    }
541    pause() {
542      console.log('NativeMediaPlayerImpl.pause');
543      this.nativePlayer.pause();
544    }
545    seek(targetTime: number) {
546      console.log(`NativeMediaPlayerImpl.seek(${targetTime})`);
547      this.nativePlayer.seek(targetTime);
548    }
549    setVolume(volume: number) {
550      console.log(`NativeMediaPlayerImpl.setVolume(${volume})`);
551      this.nativePlayer.setVolume(volume);
552    }
553    setMuted(muted: boolean) {
554      console.log(`NativeMediaPlayerImpl.setMuted(${muted})`);
555    }
556    setPlaybackRate(playbackRate: number) {
557      console.log(`NativeMediaPlayerImpl.setPlaybackRate(${playbackRate})`);
558      this.nativePlayer.setPlaybackRate(playbackRate);
559    }
560    release() {
561      console.log('NativeMediaPlayerImpl.release');
562      this.nativePlayer?.release();
563      this.nativePlayerInfo.show_native_media_player = false;
564      this.nativePlayerInfo.node_width = 300;
565      this.nativePlayerInfo.node_height = 150;
566      this.nativePlayerInfo.destroyed();
567    }
568    enterFullscreen() {
569      console.log('NativeMediaPlayerImpl.enterFullscreen');
570    }
571    exitFullscreen() {
572      console.log('NativeMediaPlayerImpl.exitFullscreen');
573    }
574  }
575
576  // 监听NativeMediaPlayer的状态,然后通过 webview.NativeMediaPlayerHandler 将状态上报给 ArkWeb 内核。
577  class AVPlayerListenerImpl implements AVPlayerListener {
578    handler: webview.NativeMediaPlayerHandler;
579    component: NativePlayerComponent;
580
581    constructor(handler: webview.NativeMediaPlayerHandler, component: NativePlayerComponent) {
582      this.handler = handler;
583      this.component = component;
584    }
585    onPlaying() {
586      console.log('AVPlayerListenerImpl.onPlaying');
587      this.handler.handleStatusChanged(webview.PlaybackStatus.PLAYING);
588    }
589    onPaused() {
590      console.log('AVPlayerListenerImpl.onPaused');
591      this.handler.handleStatusChanged(webview.PlaybackStatus.PAUSED);
592    }
593    onDurationChanged(duration: number) {
594      console.log(`AVPlayerListenerImpl.onDurationChanged(${duration})`);
595      this.handler.handleDurationChanged(duration);
596    }
597    onBufferedTimeChanged(buffered: number) {
598      console.log(`AVPlayerListenerImpl.onBufferedTimeChanged(${buffered})`);
599      this.handler.handleBufferedEndTimeChanged(buffered);
600    }
601    onTimeUpdate(time: number) {
602      this.handler.handleTimeUpdate(time);
603    }
604    onEnded() {
605      console.log('AVPlayerListenerImpl.onEnded');
606      this.handler.handleEnded();
607    }
608    onError() {
609      console.log('AVPlayerListenerImpl.onError');
610      this.component.has_error = true;
611      setTimeout(()=>{
612        this.handler.handleError(1, "Oops!");
613      }, 200);
614    }
615    onVideoSizeChanged(width: number, height: number) {
616      console.log(`AVPlayerListenerImpl.onVideoSizeChanged(${width}, ${height})`);
617      this.handler.handleVideoSizeChanged(width, height);
618      this.component.onSizeChanged(width, height);
619    }
620    onDestroyed(): void {
621      console.log('AVPlayerListenerImpl.onDestroyed');
622    }
623  }
624
625  interface ComponentParams {
626    text: string;
627    text2: string;
628    playerInfo: NativePlayerInfo;
629    handler: webview.NativeMediaPlayerHandler;
630    player: NativeMediaPlayerImpl;
631  }
632
633  // 自定义的播放器组件
634  @Component
635  struct NativePlayerComponent {
636    params?: ComponentParams;
637    @State bgColor: Color = Color.Red;
638    mXComponentController: XComponentController = new XComponentController();
639
640    videoController: VideoController = new VideoController();
641    offset_x: number = 0;
642    offset_y: number = 0;
643    @State video_width_percent: number = 100;
644    @State video_height_percent: number = 100;
645    view_width: number = 0;
646    view_height: number = 0;
647    video_width: number = 0;
648    video_height: number = 0;
649
650    fullscreen: boolean = false;
651    @State has_error: boolean = false;
652
653    onSizeChanged(width: number, height: number) {
654      this.video_width = width;
655      this.video_height = height;
656      let scale: number = this.view_width / width;
657      let scaled_video_height: number = scale * height;
658      this.video_height_percent = scaled_video_height / this.view_height * 100;
659      console.log(`NativePlayerComponent.onSizeChanged(${width},${height}), video_height_percent[${this.video_height_percent }]`);
660    }
661
662    build() {
663      Column() {
664        Stack() {
665          XComponent({ id: 'video_player_id', type: XComponentType.SURFACE, controller: this.mXComponentController })
666            .width(this.video_width_percent + '%')
667            .height(this.video_height_percent + '%')
668            .onLoad(()=>{
669              if (!this.params) {
670                console.log('this.params is null');
671                return;
672              }
673              console.log('NativePlayerComponent.onLoad, params[' + this.params
674                + '], text[' + this.params.text + '], text2[' + this.params.text2
675                + '], web_tab[' + this.params.playerInfo + '], handler[' + this.params.handler + ']');
676              this.params.player.nativePlayer.setSurfaceID(this.mXComponentController.getXComponentSurfaceId());
677
678              this.params.player.nativePlayer.avPlayerLiveDemo({
679                url: this.params.player.mediaSource,
680                listener: new AVPlayerListenerImpl(this.params.handler, this),
681                httpHeaders: this.params.player.httpHeaders,
682              });
683            })
684          Column() {
685            Row() {
686              Button(this.params?.text)
687                .height(50)
688                .border({ width: 2, color: Color.Red })
689                .backgroundColor(this.bgColor)
690                .onClick(()=>{
691                  console.log(`NativePlayerComponent.Button[${this.params?.text}] is clicked`);
692                  this.params?.player.nativePlayer?.play();
693                })
694                .onTouch((event: TouchEvent) => {
695                  event.stopPropagation();
696                })
697              Button(this.params?.text2)
698                .height(50)
699                .border({ width: 2, color: Color.Red })
700                .onClick(()=>{
701                  console.log(`NativePlayerComponent.Button[${this.params?.text2}] is clicked`);
702                  this.params?.player.nativePlayer?.pause();
703                })
704                .onTouch((event: TouchEvent) => {
705                  event.stopPropagation();
706                })
707            }
708            .width('100%')
709            .justifyContent(FlexAlign.SpaceEvenly)
710          }
711          if (this.has_error) {
712            Column() {
713              Text('Error')
714                .fontSize(30)
715            }
716            .backgroundColor('#eb5555')
717            .width('100%')
718            .height('100%')
719            .justifyContent(FlexAlign.Center)
720          }
721        }
722      }
723      .width('100%')
724      .height('100%')
725      .onAreaChange((oldValue: Area, newValue: Area) => {
726        console.log(`NativePlayerComponent.onAreaChange(${JSON.stringify(oldValue)}, ${JSON.stringify(newValue)})`);
727        this.view_width = new Number(newValue.width).valueOf();
728        this.view_height = new Number(newValue.height).valueOf();
729        this.onSizeChanged(this.video_width, this.video_height);
730      })
731    }
732  }
733
734  @Builder
735  function NativePlayerComponentBuilder(params: ComponentParams) {
736    NativePlayerComponent({ params: params })
737      .backgroundColor(Color.Green)
738      .border({ width: 1, color: Color.Brown })
739      .width('100%')
740      .height('100%')
741  }
742
743  // 通过 NodeController 来动态创建自定义的播放器组件, 并将组件内容绘制到 surfaceId 指定的 surface 上。
744  class MyNodeController extends NodeController {
745    private rootNode: BuilderNode<[ComponentParams]> | undefined;
746    playerInfo: NativePlayerInfo;
747    listener: webview.NativeMediaPlayerHandler;
748    player: NativeMediaPlayerImpl;
749
750    constructor(playerInfo: NativePlayerInfo,
751                surfaceId: string,
752                listener: webview.NativeMediaPlayerHandler,
753                player: NativeMediaPlayerImpl,
754                renderType: NodeRenderType) {
755      super();
756      this.playerInfo = playerInfo;
757      this.listener = listener;
758      this.player = player;
759      let uiContext = AppStorage.get<UIContext>("UIContext");
760      this.rootNode = new BuilderNode(uiContext as UIContext, { surfaceId: surfaceId, type: renderType });
761      console.log(`MyNodeController, rootNode[${this.rootNode}], playerInfo[${playerInfo}], listener[${listener}], surfaceId[${surfaceId}]`);
762    }
763
764    makeNode(): FrameNode | null {
765      if (this.rootNode) {
766        return this.rootNode.getFrameNode() as FrameNode;
767      }
768      return null;
769    }
770
771    build() {
772      let params: ComponentParams = {
773        "text": "play",
774        "text2": "pause",
775        playerInfo: this.playerInfo,
776        handler: this.listener,
777        player: this.player
778      };
779      if (this.rootNode) {
780        this.rootNode.build(wrapBuilder(NativePlayerComponentBuilder), params);
781      }
782    }
783
784    postTouchEvent(event: TouchEvent) {
785      return this.rootNode?.postTouchEvent(event);
786    }
787  }
788
789  class Rect {
790    x: number = 0;
791    y: number = 0;
792    width: number = 0;
793    height: number = 0;
794
795    static toNodeRect(rectInPx: webview.RectEvent, uiContext: UIContext) : Rect {
796      let rect = new Rect();
797      rect.x = uiContext.px2vp(rectInPx.x);
798      rect.y = uiContext.px2vp(rectInPx.x);
799      rect.width = uiContext.px2vp(rectInPx.width);
800      rect.height = uiContext.px2vp(rectInPx.height);
801      return rect;
802    }
803  }
804
805  @Observed
806  class NativePlayerInfo {
807    public web: WebComponent;
808    public embed_id: string;
809    public player: webview.NativeMediaPlayerBridge;
810    public node_controller?: MyNodeController;
811    public show_native_media_player: boolean = false;
812    public node_pos_x: number;
813    public node_pos_y: number;
814    public node_width: number;
815    public node_height: number;
816
817    playerComponent?: NativeMediaPlayerComponent;
818
819    constructor(web: WebComponent, handler: webview.NativeMediaPlayerHandler, videoInfo: webview.MediaInfo, uiContext: UIContext) {
820      this.web = web;
821      this.embed_id = videoInfo.embedID;
822
823      let node_rect = Rect.toNodeRect(videoInfo.surfaceInfo.rect, uiContext);
824      this.node_pos_x = node_rect.x;
825      this.node_pos_y = node_rect.y;
826      this.node_width = node_rect.width;
827      this.node_height = node_rect.height;
828
829      this.player = new NativeMediaPlayerImpl(this, handler, videoInfo, uiContext);
830    }
831
832    updateNativePlayerRect(x: number, y: number, width: number, height: number) {
833      this.playerComponent?.updateNativePlayerRect(x, y, width, height);
834    }
835
836    destroyed() {
837      let info_list = this.web.native_player_info_list;
838      console.log(`NativePlayerInfo[${this.embed_id}] destroyed, list.size[${info_list.length}]`);
839      this.web.native_player_info_list = info_list.filter((item) => item.embed_id != this.embed_id);
840      console.log(`NativePlayerInfo after destroyed, new_list.size[${this.web.native_player_info_list.length}]`);
841    }
842  }
843
844  @Component
845  struct NativeMediaPlayerComponent {
846    @ObjectLink playerInfo: NativePlayerInfo;
847
848    aboutToAppear() {
849      this.playerInfo.playerComponent = this;
850    }
851
852    build() {
853      NodeContainer(this.playerInfo.node_controller)
854        .width(this.playerInfo.node_width)
855        .height(this.playerInfo.node_height)
856        .offset({x: this.playerInfo.node_pos_x, y: this.playerInfo.node_pos_y})
857        .backgroundColor(Color.Transparent)
858        .border({ width: 2, color: Color.Orange })
859        .onAreaChange((oldValue, newValue) => {
860          console.log(`NodeContainer[${this.playerInfo.embed_id}].onAreaChange([${oldValue.width} x ${oldValue.height}]->[${newValue.width} x ${newValue.height}]`);
861        })
862    }
863
864    updateNativePlayerRect(x: number, y: number, width: number, height: number) {
865      let node_rect = Rect.toNodeRect({x, y, width, height}, this.getUIContext());
866      this.playerInfo.node_pos_x = node_rect.x;
867      this.playerInfo.node_pos_y = node_rect.y;
868      this.playerInfo.node_width = node_rect.width;
869      this.playerInfo.node_height = node_rect.height;
870    }
871  }
872
873  @Entry
874  @Component
875  struct WebComponent {
876    controller: WebviewController = new webview.WebviewController();
877    page_url: Resource = $rawfile('main.html');
878
879    @State native_player_info_list: NativePlayerInfo[] = [];
880
881    area?: Area;
882
883    build() {
884      Column() {
885        Stack({alignContent: Alignment.TopStart}) {
886          ForEach(this.native_player_info_list, (item: NativePlayerInfo) => {
887            if (item.show_native_media_player) {
888              NativeMediaPlayerComponent({ playerInfo: item })
889            }
890          }, (item: NativePlayerInfo) => {
891            return item.embed_id;
892          })
893          Web({ src: this.page_url, controller: this.controller })
894            .enableNativeMediaPlayer({ enable: true, shouldOverlay: true })
895            .onPageBegin(() => {
896              this.controller.onCreateNativeMediaPlayer((handler: webview.NativeMediaPlayerHandler, mediaInfo: webview.MediaInfo) => {
897                console.log('onCreateNativeMediaPlayer(' + JSON.stringify(mediaInfo) + ')');
898                let nativePlayerInfo = new NativePlayerInfo(this, handler, mediaInfo, this.getUIContext());
899                this.native_player_info_list.push(nativePlayerInfo);
900                return nativePlayerInfo.player;
901              });
902            })
903            .onNativeEmbedGestureEvent((event)=>{
904              if (!event.touchEvent || !event.embedId) {
905                event.result?.setGestureEventResult(false);
906                return;
907              }
908              console.log(`WebComponent.onNativeEmbedGestureEvent, embedId[${event.embedId}]`);
909              let native_player_info = this.getNativePlayerInfoByEmbedId(event.embedId);
910              if (!native_player_info) {
911                console.log(`WebComponent.onNativeEmbedGestureEvent, embedId[${event.embedId}], no native_player_info`);
912                event.result?.setGestureEventResult(false);
913                return;
914              }
915              if (!native_player_info.node_controller) {
916                console.log(`WebComponent.onNativeEmbedGestureEvent, embedId[${event.embedId}], no node_controller`);
917                event.result?.setGestureEventResult(false);
918                return;
919              }
920              let ret = native_player_info.node_controller.postTouchEvent(event.touchEvent);
921              console.log(`WebComponent.postTouchEvent, ret[${ret}], touchEvent[${JSON.stringify(event.touchEvent)}]`);
922              event.result?.setGestureEventResult(ret);
923            })
924            .width('100%')
925            .height('100%')
926            .onAreaChange((oldValue: Area, newValue: Area) => {
927              oldValue;
928              this.area = newValue;
929            })
930        }
931      }
932    }
933
934    getNativePlayerInfoByEmbedId(embedId: string) : NativePlayerInfo | undefined {
935      return this.native_player_info_list.find((item)=> item.embed_id == embedId);
936    }
937  }
938  ```
939
940- 应用侧代码,视频播放示例, ./PlayerDemo.ets941
942  ```ts
943  import { media } from '@kit.MediaKit';
944  import { BusinessError } from '@kit.BasicServicesKit';
945
946  export interface AVPlayerListener {
947    onPlaying() : void;
948    onPaused() : void;
949    onDurationChanged(duration: number) : void;
950    onBufferedTimeChanged(buffered: number) : void;
951    onTimeUpdate(time: number) : void;
952    onEnded() : void;
953    onError() : void;
954    onVideoSizeChanged(width: number, height: number): void;
955    onDestroyed(): void;
956  }
957
958  interface PlayerParam {
959    url: string;
960    listener?: AVPlayerListener;
961    httpHeaders?: Record<string, string>;
962  }
963
964  interface PlayCommand {
965    func: Function;
966    name?: string;
967  }
968
969  interface CheckPlayCommandResult {
970    ignore: boolean;
971    index_to_remove: number;
972  }
973
974  export class AVPlayerDemo {
975    private surfaceID: string = ''; // surfaceID用于播放画面显示,具体的值需要通过Xcomponent接口获取,相关文档链接见上面Xcomponent创建方法
976
977    avPlayer?: media.AVPlayer;
978    prepared: boolean = false;
979
980    commands: PlayCommand[] = [];
981
982    setSurfaceID(surface_id: string) {
983      console.log(`AVPlayerDemo.setSurfaceID : ${surface_id}`);
984      this.surfaceID = surface_id;
985    }
986    // 注册avplayer回调函数
987    setAVPlayerCallback(avPlayer: media.AVPlayer, listener?: AVPlayerListener) {
988      // seek操作结果回调函数
989      avPlayer.on('seekDone', (seekDoneTime: number) => {
990        console.info(`AVPlayer seek succeeded, seek time is ${seekDoneTime}`);
991      });
992      // error回调监听函数,当avPlayer在操作过程中出现错误时调用reset接口触发重置流程
993      avPlayer.on('error', (err: BusinessError) => {
994        console.error(`Invoke avPlayer failed, code is ${err.code}, message is ${err.message}`);
995        listener?.onError();
996        avPlayer.reset(); // 调用reset重置资源,触发idle状态
997      });
998      // 状态机变化回调函数
999      avPlayer.on('stateChange', async (state: string, reason: media.StateChangeReason) => {
1000        switch (state) {
1001          case 'idle': // 成功调用reset接口后触发该状态机上报
1002            console.info('AVPlayer state idle called.');
1003            avPlayer.release(); // 调用release接口销毁实例对象
1004            break;
1005          case 'initialized': // avplayer 设置播放源后触发该状态上报
1006            console.info('AVPlayer state initialized called.');
1007            avPlayer.surfaceId = this.surfaceID; // 设置显示画面,当播放的资源为纯音频时无需设置
1008            avPlayer.prepare();
1009            break;
1010          case 'prepared': // prepare调用成功后上报该状态机
1011            console.info('AVPlayer state prepared called.');
1012            this.prepared = true;
1013            this.schedule();
1014            break;
1015          case 'playing': // play成功调用后触发该状态机上报
1016            console.info('AVPlayer state playing called.');
1017            listener?.onPlaying();
1018            break;
1019          case 'paused': // pause成功调用后触发该状态机上报
1020            console.info('AVPlayer state paused called.');
1021            listener?.onPaused();
1022            break;
1023          case 'completed': // 播放结束后触发该状态机上报
1024            console.info('AVPlayer state completed called.');
1025            avPlayer.stop(); //调用播放结束接口
1026            break;
1027          case 'stopped': // stop接口成功调用后触发该状态机上报
1028            console.info('AVPlayer state stopped called.');
1029            listener?.onEnded();
1030            break;
1031          case 'released':
1032            this.prepared = false;
1033            listener?.onDestroyed();
1034            console.info('AVPlayer state released called.');
1035            break;
1036          default:
1037            console.info('AVPlayer state unknown called.');
1038            break;
1039        }
1040      });
1041      avPlayer.on('durationUpdate', (duration: number) => {
1042        console.info(`AVPlayer state durationUpdate success,new duration is :${duration}`);
1043        listener?.onDurationChanged(duration/1000);
1044      });
1045      avPlayer.on('timeUpdate', (time:number) => {
1046        listener?.onTimeUpdate(time/1000);
1047      });
1048      avPlayer.on('bufferingUpdate', (infoType: media.BufferingInfoType, value: number) => {
1049        console.info(`AVPlayer state bufferingUpdate success,and infoType value is:${infoType}, value is : ${value}`);
1050        if (infoType == media.BufferingInfoType.BUFFERING_PERCENT) {
1051        }
1052        listener?.onBufferedTimeChanged(value);
1053      })
1054      avPlayer.on('videoSizeChange', (width: number, height: number) => {
1055        console.info(`AVPlayer state videoSizeChange success,and width is:${width}, height is : ${height}`);
1056        listener?.onVideoSizeChanged(width, height);
1057      })
1058    }
1059
1060    // 以下demo为通过url设置网络地址来实现播放直播码流的demo
1061    async avPlayerLiveDemo(playerParam: PlayerParam) {
1062      // 创建avPlayer实例对象
1063      this.avPlayer = await media.createAVPlayer();
1064      // 创建状态机变化回调函数
1065      this.setAVPlayerCallback(this.avPlayer, playerParam.listener);
1066
1067      let mediaSource: media.MediaSource = media.createMediaSourceWithUrl(playerParam.url, playerParam.httpHeaders);
1068      let strategy: media.PlaybackStrategy = {
1069        preferredWidth: 100,
1070        preferredHeight: 100,
1071        preferredBufferDuration: 100,
1072        preferredHdr: false
1073      };
1074      this.avPlayer.setMediaSource(mediaSource, strategy);
1075      console.log(`AVPlayer url:[${playerParam.url}]`);
1076    }
1077
1078    schedule() {
1079      if (!this.avPlayer) {
1080        return;
1081      }
1082      if (!this.prepared) {
1083        return;
1084      }
1085      if (this.commands.length > 0) {
1086        let command = this.commands.shift();
1087        if (command) {
1088          command.func();
1089        }
1090        if (this.commands.length > 0) {
1091          setTimeout(() => {
1092            this.schedule();
1093          });
1094        }
1095      }
1096    }
1097
1098    private checkCommand(selfName: string, oppositeName: string) {
1099      let index_to_remove = -1;
1100      let ignore_this_action = false;
1101      let index = this.commands.length - 1;
1102      while (index >= 0) {
1103        if (this.commands[index].name == selfName) {
1104          ignore_this_action = true;
1105          break;
1106        }
1107        if (this.commands[index].name == oppositeName) {
1108          index_to_remove = index;
1109          break;
1110        }
1111        index--;
1112      }
1113
1114      let result : CheckPlayCommandResult = {
1115        ignore: ignore_this_action,
1116        index_to_remove: index_to_remove,
1117      };
1118      return result;
1119    }
1120
1121    play() {
1122      let commandName = 'play';
1123      let checkResult = this.checkCommand(commandName, 'pause');
1124      if (checkResult.ignore) {
1125        console.log(`AVPlayer ${commandName} ignored.`);
1126        this.schedule();
1127        return;
1128      }
1129      if (checkResult.index_to_remove >= 0) {
1130        let removedCommand = this.commands.splice(checkResult.index_to_remove, 1);
1131        console.log(`AVPlayer ${JSON.stringify(removedCommand)} removed.`);
1132        return;
1133      }
1134      this.commands.push({ func: ()=>{
1135        console.info('AVPlayer.play()');
1136        this.avPlayer?.play();
1137      }, name: commandName});
1138      this.schedule();
1139    }
1140    pause() {
1141      let commandName = 'pause';
1142      let checkResult = this.checkCommand(commandName, 'play');
1143      console.log(`checkResult:${JSON.stringify(checkResult)}`);
1144      if (checkResult.ignore) {
1145        console.log(`AVPlayer ${commandName} ignored.`);
1146        this.schedule();
1147        return;
1148      }
1149      if (checkResult.index_to_remove >= 0) {
1150        let removedCommand = this.commands.splice(checkResult.index_to_remove, 1);
1151        console.log(`AVPlayer ${JSON.stringify(removedCommand)} removed.`);
1152        return;
1153      }
1154      this.commands.push({ func: ()=>{
1155        console.info('AVPlayer.pause()');
1156        this.avPlayer?.pause();
1157      }, name: commandName});
1158      this.schedule();
1159    }
1160    release() {
1161      this.commands.push({ func: ()=>{
1162        console.info('AVPlayer.release()');
1163        this.avPlayer?.release();
1164      }});
1165      this.schedule();
1166    }
1167    seek(time: number) {
1168      this.commands.push({ func: ()=>{
1169        console.info(`AVPlayer.seek(${time})`);
1170        this.avPlayer?.seek(time * 1000);
1171      }});
1172      this.schedule();
1173    }
1174    setVolume(volume: number) {
1175      this.commands.push({ func: ()=>{
1176        console.info(`AVPlayer.setVolume(${volume})`);
1177        this.avPlayer?.setVolume(volume);
1178      }});
1179      this.schedule();
1180    }
1181    setPlaybackRate(playbackRate: number) {
1182      let speed = media.PlaybackSpeed.SPEED_FORWARD_1_00_X;
1183      let delta = 0.05;
1184      playbackRate += delta;
1185      if (playbackRate < 1) {
1186        speed = media.PlaybackSpeed.SPEED_FORWARD_0_75_X;
1187      } else if (playbackRate < 1.25) {
1188        speed = media.PlaybackSpeed.SPEED_FORWARD_1_00_X;
1189      } else if (playbackRate < 1.5) {
1190        speed = media.PlaybackSpeed.SPEED_FORWARD_1_25_X;
1191      } else if (playbackRate < 2) {
1192        speed = media.PlaybackSpeed.SPEED_FORWARD_1_75_X;
1193      } else {
1194        speed = media.PlaybackSpeed.SPEED_FORWARD_2_00_X;
1195      }
1196      this.commands.push({ func: ()=>{
1197        console.info(`AVPlayer.setSpeed(${speed})`);
1198        this.avPlayer?.setSpeed(speed);
1199      }});
1200      this.schedule();
1201    }
1202  }
1203  ```
1204
1205- 前端页面示例。
1206
1207  ```html
1208  <html>
1209  <head>
1210      <title>视频托管测试html</title>
1211      <meta name="viewport" content="width=device-width">
1212  </head>
1213  <body>
1214  <div>
1215      <!-- 使用时需要自行替换视频链接 -->
1216      <video src='https://xxx.xxx/demo.mp4' style='width: 100%'></video>
1217  </div>
1218  </body>
1219  </html>
1220  ```
1221