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