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
495  import { webview } from '@kit.ArkWeb';
496  import { BuilderNode, FrameNode, NodeController, NodeRenderType, UIContext } from '@kit.ArkUI';
497  import { AVPlayerDemo, AVPlayerListener } from './PlayerDemo';
498
499  // 实现 webview.NativeMediaPlayerBridge 接口。
500  // ArkWeb 内核调用该类的方法来对 NativeMediaPlayer 进行播控。
501  class NativeMediaPlayerImpl implements webview.NativeMediaPlayerBridge {
502    private surfaceId: string;
503    mediaSource: string;
504    private mediaHandler: webview.NativeMediaPlayerHandler;
505    nativePlayerInfo: NativePlayerInfo;
506    nativePlayer: AVPlayerDemo;
507    httpHeaders: Record<string, string>;
508
509    constructor(nativePlayerInfo: NativePlayerInfo, handler: webview.NativeMediaPlayerHandler, mediaInfo: webview.MediaInfo) {
510      console.log(`NativeMediaPlayerImpl.constructor, surface_id[${mediaInfo.surfaceInfo.id}]`);
511      this.nativePlayerInfo = nativePlayerInfo;
512      this.mediaHandler = handler;
513      this.surfaceId = mediaInfo.surfaceInfo.id;
514      this.mediaSource = mediaInfo.mediaSrcList.find((item)=>{item.source.indexOf('.mp4') > 0})?.source
515        || mediaInfo.mediaSrcList[0].source;
516      this.httpHeaders = mediaInfo.headers;
517      this.nativePlayer = new AVPlayerDemo();
518
519      // 使用同层渲染功能,将视频及其播控组件绘制到网页中
520      this.nativePlayerInfo.node_controller = new MyNodeController(
521        this.nativePlayerInfo, this.surfaceId, this.mediaHandler, this, NodeRenderType.RENDER_TYPE_TEXTURE);
522      this.nativePlayerInfo.node_controller.build();
523      this.nativePlayerInfo.show_native_media_player = true;
524
525      console.log(`NativeMediaPlayerImpl.mediaSource: ${this.mediaSource}, headers: ${JSON.stringify(this.httpHeaders)}`);
526    }
527
528    updateRect(x: number, y: number, width: number, height: number): void {
529      let width_in_vp = px2vp(width);
530      let height_in_vp = px2vp(height);
531      console.log(`updateRect(${x}, ${y}, ${width}, ${height}), vp:{${width_in_vp}, ${height_in_vp}}`);
532
533      this.nativePlayerInfo.updateNativePlayerRect(x, y, width, height);
534    }
535
536    play() {
537      console.log('NativeMediaPlayerImpl.play');
538      this.nativePlayer.play();
539    }
540    pause() {
541      console.log('NativeMediaPlayerImpl.pause');
542      this.nativePlayer.pause();
543    }
544    seek(targetTime: number) {
545      console.log(`NativeMediaPlayerImpl.seek(${targetTime})`);
546      this.nativePlayer.seek(targetTime);
547    }
548    setVolume(volume: number) {
549      console.log(`NativeMediaPlayerImpl.setVolume(${volume})`);
550      this.nativePlayer.setVolume(volume);
551    }
552    setMuted(muted: boolean) {
553      console.log(`NativeMediaPlayerImpl.setMuted(${muted})`);
554    }
555    setPlaybackRate(playbackRate: number) {
556      console.log(`NativeMediaPlayerImpl.setPlaybackRate(${playbackRate})`);
557      this.nativePlayer.setPlaybackRate(playbackRate);
558    }
559    release() {
560      console.log('NativeMediaPlayerImpl.release');
561      this.nativePlayer?.release();
562      this.nativePlayerInfo.show_native_media_player = false;
563      this.nativePlayerInfo.node_width = 300;
564      this.nativePlayerInfo.node_height = 150;
565      this.nativePlayerInfo.destroyed();
566    }
567    enterFullscreen() {
568      console.log('NativeMediaPlayerImpl.enterFullscreen');
569    }
570    exitFullscreen() {
571      console.log('NativeMediaPlayerImpl.exitFullscreen');
572    }
573  }
574
575  // 监听NativeMediaPlayer的状态,然后通过 webview.NativeMediaPlayerHandler 将状态上报给 ArkWeb 内核。
576  class AVPlayerListenerImpl implements AVPlayerListener {
577    handler: webview.NativeMediaPlayerHandler;
578    component: NativePlayerComponent;
579
580    constructor(handler: webview.NativeMediaPlayerHandler, component: NativePlayerComponent) {
581      this.handler = handler;
582      this.component = component;
583    }
584    onPlaying() {
585      console.log('AVPlayerListenerImpl.onPlaying');
586      this.handler.handleStatusChanged(webview.PlaybackStatus.PLAYING);
587    }
588    onPaused() {
589      console.log('AVPlayerListenerImpl.onPaused');
590      this.handler.handleStatusChanged(webview.PlaybackStatus.PAUSED);
591    }
592    onDurationChanged(duration: number) {
593      console.log(`AVPlayerListenerImpl.onDurationChanged(${duration})`);
594      this.handler.handleDurationChanged(duration);
595    }
596    onBufferedTimeChanged(buffered: number) {
597      console.log(`AVPlayerListenerImpl.onBufferedTimeChanged(${buffered})`);
598      this.handler.handleBufferedEndTimeChanged(buffered);
599    }
600    onTimeUpdate(time: number) {
601      this.handler.handleTimeUpdate(time);
602    }
603    onEnded() {
604      console.log('AVPlayerListenerImpl.onEnded');
605      this.handler.handleEnded();
606    }
607    onError() {
608      console.log('AVPlayerListenerImpl.onError');
609      this.component.has_error = true;
610      setTimeout(()=>{
611        this.handler.handleError(1, "Oops!");
612      }, 200);
613    }
614    onVideoSizeChanged(width: number, height: number) {
615      console.log(`AVPlayerListenerImpl.onVideoSizeChanged(${width}, ${height})`);
616      this.handler.handleVideoSizeChanged(width, height);
617      this.component.onSizeChanged(width, height);
618    }
619    onDestroyed(): void {
620      console.log('AVPlayerListenerImpl.onDestroyed');
621    }
622  }
623
624  interface ComponentParams {
625    text: string;
626    text2: string;
627    playerInfo: NativePlayerInfo;
628    handler: webview.NativeMediaPlayerHandler;
629    player: NativeMediaPlayerImpl;
630  }
631
632  // 自定义的播放器组件
633  @Component
634  struct NativePlayerComponent {
635    params?: ComponentParams;
636    @State bgColor: Color = Color.Red;
637    mXComponentController: XComponentController = new XComponentController();
638
639    videoController: VideoController = new VideoController();
640    offset_x: number = 0;
641    offset_y: number = 0;
642    @State video_width_percent: number = 100;
643    @State video_height_percent: number = 100;
644    view_width: number = 0;
645    view_height: number = 0;
646    video_width: number = 0;
647    video_height: number = 0;
648
649    fullscreen: boolean = false;
650    @State has_error: boolean = false;
651
652    onSizeChanged(width: number, height: number) {
653      this.video_width = width;
654      this.video_height = height;
655      let scale: number = this.view_width / width;
656      let scaled_video_height: number = scale * height;
657      this.video_height_percent = scaled_video_height / this.view_height * 100;
658      console.log(`NativePlayerComponent.onSizeChanged(${width},${height}), video_height_percent[${this.video_height_percent }]`);
659    }
660
661    build() {
662      Column() {
663        Stack() {
664          XComponent({ id: 'video_player_id', type: XComponentType.SURFACE, controller: this.mXComponentController })
665            .width(this.video_width_percent + '%')
666            .height(this.video_height_percent + '%')
667            .onLoad(()=>{
668              if (!this.params) {
669                console.log('this.params is null');
670                return;
671              }
672              console.log('NativePlayerComponent.onLoad, params[' + this.params
673                + '], text[' + this.params.text + '], text2[' + this.params.text2
674                + '], web_tab[' + this.params.playerInfo + '], handler[' + this.params.handler + ']');
675              this.params.player.nativePlayer.setSurfaceID(this.mXComponentController.getXComponentSurfaceId());
676
677              this.params.player.nativePlayer.avPlayerLiveDemo({
678                url: this.params.player.mediaSource,
679                listener: new AVPlayerListenerImpl(this.params.handler, this),
680                httpHeaders: this.params.player.httpHeaders,
681              });
682            })
683          Column() {
684            Row() {
685              Button(this.params?.text)
686                .height(50)
687                .border({ width: 2, color: Color.Red })
688                .backgroundColor(this.bgColor)
689                .onClick(()=>{
690                  console.log(`NativePlayerComponent.Button[${this.params?.text}] is clicked`);
691                  this.params?.player.nativePlayer?.play();
692                })
693                .onTouch((event: TouchEvent) => {
694                  event.stopPropagation();
695                })
696              Button(this.params?.text2)
697                .height(50)
698                .border({ width: 2, color: Color.Red })
699                .onClick(()=>{
700                  console.log(`NativePlayerComponent.Button[${this.params?.text2}] is clicked`);
701                  this.params?.player.nativePlayer?.pause();
702                })
703                .onTouch((event: TouchEvent) => {
704                  event.stopPropagation();
705                })
706            }
707            .width('100%')
708            .justifyContent(FlexAlign.SpaceEvenly)
709          }
710          if (this.has_error) {
711            Column() {
712              Text('Error')
713                .fontSize(30)
714            }
715            .backgroundColor('#eb5555')
716            .width('100%')
717            .height('100%')
718            .justifyContent(FlexAlign.Center)
719          }
720        }
721      }
722      .width('100%')
723      .height('100%')
724      .onAreaChange((oldValue: Area, newValue: Area) => {
725        console.log(`NativePlayerComponent.onAreaChange(${JSON.stringify(oldValue)}, ${JSON.stringify(newValue)})`);
726        this.view_width = new Number(newValue.width).valueOf();
727        this.view_height = new Number(newValue.height).valueOf();
728        this.onSizeChanged(this.video_width, this.video_height);
729      })
730    }
731  }
732
733  @Builder
734  function NativePlayerComponentBuilder(params: ComponentParams) {
735    NativePlayerComponent({ params: params })
736      .backgroundColor(Color.Green)
737      .border({ width: 1, color: Color.Brown })
738      .width('100%')
739      .height('100%')
740  }
741
742  // 通过 NodeController 来动态创建自定义的播放器组件, 并将组件内容绘制到 surfaceId 指定的 surface 上。
743  class MyNodeController extends NodeController {
744    private rootNode: BuilderNode<[ComponentParams]> | undefined;
745    playerInfo: NativePlayerInfo;
746    listener: webview.NativeMediaPlayerHandler;
747    player: NativeMediaPlayerImpl;
748
749    constructor(playerInfo: NativePlayerInfo,
750                surfaceId: string,
751                listener: webview.NativeMediaPlayerHandler,
752                player: NativeMediaPlayerImpl,
753                renderType: NodeRenderType) {
754      super();
755      this.playerInfo = playerInfo;
756      this.listener = listener;
757      this.player = player;
758      let uiContext = AppStorage.get<UIContext>("UIContext");
759      this.rootNode = new BuilderNode(uiContext as UIContext, { surfaceId: surfaceId, type: renderType });
760      console.log(`MyNodeController, rootNode[${this.rootNode}], playerInfo[${playerInfo}], listener[${listener}], surfaceId[${surfaceId}]`);
761    }
762
763    makeNode(): FrameNode | null {
764      if (this.rootNode) {
765        return this.rootNode.getFrameNode() as FrameNode;
766      }
767      return null;
768    }
769
770    build() {
771      let params: ComponentParams = {
772        "text": "play",
773        "text2": "pause",
774        playerInfo: this.playerInfo,
775        handler: this.listener,
776        player: this.player
777      };
778      if (this.rootNode) {
779        this.rootNode.build(wrapBuilder(NativePlayerComponentBuilder), params);
780      }
781    }
782
783    postTouchEvent(event: TouchEvent) {
784      return this.rootNode?.postTouchEvent(event);
785    }
786  }
787
788  class Rect {
789    x: number = 0;
790    y: number = 0;
791    width: number = 0;
792    height: number = 0;
793
794    static toNodeRect(rectInPx: webview.RectEvent) : Rect {
795      let rect = new Rect();
796      rect.x = px2vp(rectInPx.x);
797      rect.y = px2vp(rectInPx.x);
798      rect.width = px2vp(rectInPx.width);
799      rect.height = px2vp(rectInPx.height);
800      return rect;
801    }
802  }
803
804  @Observed
805  class NativePlayerInfo {
806    public web: WebComponent;
807    public embed_id: string;
808    public player: webview.NativeMediaPlayerBridge;
809    public node_controller?: MyNodeController;
810    public show_native_media_player: boolean = false;
811    public node_pos_x: number;
812    public node_pos_y: number;
813    public node_width: number;
814    public node_height: number;
815
816    playerComponent?: NativeMediaPlayerComponent;
817
818    constructor(web: WebComponent, handler: webview.NativeMediaPlayerHandler, videoInfo: webview.MediaInfo) {
819      this.web = web;
820      this.embed_id = videoInfo.embedID;
821
822      let node_rect = Rect.toNodeRect(videoInfo.surfaceInfo.rect);
823      this.node_pos_x = node_rect.x;
824      this.node_pos_y = node_rect.y;
825      this.node_width = node_rect.width;
826      this.node_height = node_rect.height;
827
828      this.player = new NativeMediaPlayerImpl(this, handler, videoInfo);
829    }
830
831    updateNativePlayerRect(x: number, y: number, width: number, height: number) {
832      this.playerComponent?.updateNativePlayerRect(x, y, width, height);
833    }
834
835    destroyed() {
836      let info_list = this.web.native_player_info_list;
837      console.log(`NativePlayerInfo[${this.embed_id}] destroyed, list.size[${info_list.length}]`);
838      this.web.native_player_info_list = info_list.filter((item) => item.embed_id != this.embed_id);
839      console.log(`NativePlayerInfo after destroyed, new_list.size[${this.web.native_player_info_list.length}]`);
840    }
841  }
842
843  @Component
844  struct NativeMediaPlayerComponent {
845    @ObjectLink playerInfo: NativePlayerInfo;
846
847    aboutToAppear() {
848      this.playerInfo.playerComponent = this;
849    }
850
851    build() {
852      NodeContainer(this.playerInfo.node_controller)
853        .width(this.playerInfo.node_width)
854        .height(this.playerInfo.node_height)
855        .offset({x: this.playerInfo.node_pos_x, y: this.playerInfo.node_pos_y})
856        .backgroundColor(Color.Transparent)
857        .border({ width: 2, color: Color.Orange })
858        .onAreaChange((oldValue, newValue) => {
859          console.log(`NodeContainer[${this.playerInfo.embed_id}].onAreaChange([${oldValue.width} x ${oldValue.height}]->[${newValue.width} x ${newValue.height}]`);
860        })
861    }
862
863    updateNativePlayerRect(x: number, y: number, width: number, height: number) {
864      let node_rect = Rect.toNodeRect({x, y, width, height});
865      this.playerInfo.node_pos_x = node_rect.x;
866      this.playerInfo.node_pos_y = node_rect.y;
867      this.playerInfo.node_width = node_rect.width;
868      this.playerInfo.node_height = node_rect.height;
869    }
870  }
871
872  @Entry
873  @Component
874  struct WebComponent {
875    controller: WebviewController = new webview.WebviewController();
876    page_url: Resource = $rawfile('main.html');
877
878    @State native_player_info_list: NativePlayerInfo[] = [];
879
880    area?: Area;
881
882    build() {
883      Column() {
884        Stack({alignContent: Alignment.TopStart}) {
885          ForEach(this.native_player_info_list, (item: NativePlayerInfo) => {
886            if (item.show_native_media_player) {
887              NativeMediaPlayerComponent({ playerInfo: item })
888            }
889          }, (item: NativePlayerInfo) => {
890            return item.embed_id;
891          })
892          Web({ src: this.page_url, controller: this.controller })
893            .enableNativeMediaPlayer({ enable: true, shouldOverlay: true })
894            .onPageBegin(() => {
895              this.controller.onCreateNativeMediaPlayer((handler: webview.NativeMediaPlayerHandler, mediaInfo: webview.MediaInfo) => {
896                console.log('onCreateNativeMediaPlayer(' + JSON.stringify(mediaInfo) + ')');
897                let nativePlayerInfo = new NativePlayerInfo(this, handler, mediaInfo);
898                this.native_player_info_list.push(nativePlayerInfo);
899                return nativePlayerInfo.player;
900              });
901            })
902            .onNativeEmbedGestureEvent((event)=>{
903              if (!event.touchEvent || !event.embedId) {
904                event.result?.setGestureEventResult(false);
905                return;
906              }
907              console.log(`WebComponent.onNativeEmbedGestureEvent, embedId[${event.embedId}]`);
908              let native_player_info = this.getNativePlayerInfoByEmbedId(event.embedId);
909              if (!native_player_info) {
910                console.log(`WebComponent.onNativeEmbedGestureEvent, embedId[${event.embedId}], no native_player_info`);
911                event.result?.setGestureEventResult(false);
912                return;
913              }
914              if (!native_player_info.node_controller) {
915                console.log(`WebComponent.onNativeEmbedGestureEvent, embedId[${event.embedId}], no node_controller`);
916                event.result?.setGestureEventResult(false);
917                return;
918              }
919              let ret = native_player_info.node_controller.postTouchEvent(event.touchEvent);
920              console.log(`WebComponent.postTouchEvent, ret[${ret}], touchEvent[${JSON.stringify(event.touchEvent)}]`);
921              event.result?.setGestureEventResult(ret);
922            })
923            .width('100%')
924            .height('100%')
925            .onAreaChange((oldValue: Area, newValue: Area) => {
926              oldValue;
927              this.area = newValue;
928            })
929        }
930      }
931    }
932
933    getNativePlayerInfoByEmbedId(embedId: string) : NativePlayerInfo | undefined {
934      return this.native_player_info_list.find((item)=> item.embed_id == embedId);
935    }
936  }
937  ```
938
939- 应用侧代码,视频播放示例, ./PlayerDemo.ets940
941  ```ts
942  import { media } from '@kit.MediaKit';
943  import { BusinessError } from '@kit.BasicServicesKit';
944
945  export interface AVPlayerListener {
946    onPlaying() : void;
947    onPaused() : void;
948    onDurationChanged(duration: number) : void;
949    onBufferedTimeChanged(buffered: number) : void;
950    onTimeUpdate(time: number) : void;
951    onEnded() : void;
952    onError() : void;
953    onVideoSizeChanged(width: number, height: number): void;
954    onDestroyed(): void;
955  }
956
957  interface PlayerParam {
958    url: string;
959    listener?: AVPlayerListener;
960    httpHeaders?: Record<string, string>;
961  }
962
963  interface PlayCommand {
964    func: Function;
965    name?: string;
966  }
967
968  interface CheckPlayCommandResult {
969    ignore: boolean;
970    index_to_remove: number;
971  }
972
973  export class AVPlayerDemo {
974    private surfaceID: string = ''; // surfaceID用于播放画面显示,具体的值需要通过Xcomponent接口获取,相关文档链接见上面Xcomponent创建方法
975
976    avPlayer?: media.AVPlayer;
977    prepared: boolean = false;
978
979    commands: PlayCommand[] = [];
980
981    setSurfaceID(surface_id: string) {
982      console.log(`AVPlayerDemo.setSurfaceID : ${surface_id}`);
983      this.surfaceID = surface_id;
984    }
985    // 注册avplayer回调函数
986    setAVPlayerCallback(avPlayer: media.AVPlayer, listener?: AVPlayerListener) {
987      // seek操作结果回调函数
988      avPlayer.on('seekDone', (seekDoneTime: number) => {
989        console.info(`AVPlayer seek succeeded, seek time is ${seekDoneTime}`);
990      });
991      // error回调监听函数,当avPlayer在操作过程中出现错误时调用reset接口触发重置流程
992      avPlayer.on('error', (err: BusinessError) => {
993        console.error(`Invoke avPlayer failed, code is ${err.code}, message is ${err.message}`);
994        listener?.onError();
995        avPlayer.reset(); // 调用reset重置资源,触发idle状态
996      });
997      // 状态机变化回调函数
998      avPlayer.on('stateChange', async (state: string, reason: media.StateChangeReason) => {
999        switch (state) {
1000          case 'idle': // 成功调用reset接口后触发该状态机上报
1001            console.info('AVPlayer state idle called.');
1002            avPlayer.release(); // 调用release接口销毁实例对象
1003            break;
1004          case 'initialized': // avplayer 设置播放源后触发该状态上报
1005            console.info('AVPlayer state initialized called.');
1006            avPlayer.surfaceId = this.surfaceID; // 设置显示画面,当播放的资源为纯音频时无需设置
1007            avPlayer.prepare();
1008            break;
1009          case 'prepared': // prepare调用成功后上报该状态机
1010            console.info('AVPlayer state prepared called.');
1011            this.prepared = true;
1012            this.schedule();
1013            break;
1014          case 'playing': // play成功调用后触发该状态机上报
1015            console.info('AVPlayer state playing called.');
1016            listener?.onPlaying();
1017            break;
1018          case 'paused': // pause成功调用后触发该状态机上报
1019            console.info('AVPlayer state paused called.');
1020            listener?.onPaused();
1021            break;
1022          case 'completed': // 播放结束后触发该状态机上报
1023            console.info('AVPlayer state completed called.');
1024            avPlayer.stop(); //调用播放结束接口
1025            break;
1026          case 'stopped': // stop接口成功调用后触发该状态机上报
1027            console.info('AVPlayer state stopped called.');
1028            listener?.onEnded();
1029            break;
1030          case 'released':
1031            this.prepared = false;
1032            listener?.onDestroyed();
1033            console.info('AVPlayer state released called.');
1034            break;
1035          default:
1036            console.info('AVPlayer state unknown called.');
1037            break;
1038        }
1039      });
1040      avPlayer.on('durationUpdate', (duration: number) => {
1041        console.info(`AVPlayer state durationUpdate success,new duration is :${duration}`);
1042        listener?.onDurationChanged(duration/1000);
1043      });
1044      avPlayer.on('timeUpdate', (time:number) => {
1045        listener?.onTimeUpdate(time/1000);
1046      });
1047      avPlayer.on('bufferingUpdate', (infoType: media.BufferingInfoType, value: number) => {
1048        console.info(`AVPlayer state bufferingUpdate success,and infoType value is:${infoType}, value is : ${value}`);
1049        if (infoType == media.BufferingInfoType.BUFFERING_PERCENT) {
1050        }
1051        listener?.onBufferedTimeChanged(value);
1052      })
1053      avPlayer.on('videoSizeChange', (width: number, height: number) => {
1054        console.info(`AVPlayer state videoSizeChange success,and width is:${width}, height is : ${height}`);
1055        listener?.onVideoSizeChanged(width, height);
1056      })
1057    }
1058
1059    // 以下demo为通过url设置网络地址来实现播放直播码流的demo
1060    async avPlayerLiveDemo(playerParam: PlayerParam) {
1061      // 创建avPlayer实例对象
1062      this.avPlayer = await media.createAVPlayer();
1063      // 创建状态机变化回调函数
1064      this.setAVPlayerCallback(this.avPlayer, playerParam.listener);
1065
1066      let mediaSource: media.MediaSource = media.createMediaSourceWithUrl(playerParam.url, playerParam.httpHeaders);
1067      let strategy: media.PlaybackStrategy = {
1068        preferredWidth: 100,
1069        preferredHeight: 100,
1070        preferredBufferDuration: 100,
1071        preferredHdr: false
1072      };
1073      this.avPlayer.setMediaSource(mediaSource, strategy);
1074      console.log(`AVPlayer url:[${playerParam.url}]`);
1075    }
1076
1077    schedule() {
1078      if (!this.avPlayer) {
1079        return;
1080      }
1081      if (!this.prepared) {
1082        return;
1083      }
1084      if (this.commands.length > 0) {
1085        let command = this.commands.shift();
1086        if (command) {
1087          command.func();
1088        }
1089        if (this.commands.length > 0) {
1090          setTimeout(() => {
1091            this.schedule();
1092          });
1093        }
1094      }
1095    }
1096
1097    private checkCommand(selfName: string, oppositeName: string) {
1098      let index_to_remove = -1;
1099      let ignore_this_action = false;
1100      let index = this.commands.length - 1;
1101      while (index >= 0) {
1102        if (this.commands[index].name == selfName) {
1103          ignore_this_action = true;
1104          break;
1105        }
1106        if (this.commands[index].name == oppositeName) {
1107          index_to_remove = index;
1108          break;
1109        }
1110        index--;
1111      }
1112
1113      let result : CheckPlayCommandResult = {
1114        ignore: ignore_this_action,
1115        index_to_remove: index_to_remove,
1116      };
1117      return result;
1118    }
1119
1120    play() {
1121      let commandName = 'play';
1122      let checkResult = this.checkCommand(commandName, 'pause');
1123      if (checkResult.ignore) {
1124        console.log(`AVPlayer ${commandName} ignored.`);
1125        this.schedule();
1126        return;
1127      }
1128      if (checkResult.index_to_remove >= 0) {
1129        let removedCommand = this.commands.splice(checkResult.index_to_remove, 1);
1130        console.log(`AVPlayer ${JSON.stringify(removedCommand)} removed.`);
1131        return;
1132      }
1133      this.commands.push({ func: ()=>{
1134        console.info('AVPlayer.play()');
1135        this.avPlayer?.play();
1136      }, name: commandName});
1137      this.schedule();
1138    }
1139    pause() {
1140      let commandName = 'pause';
1141      let checkResult = this.checkCommand(commandName, 'play');
1142      console.log(`checkResult:${JSON.stringify(checkResult)}`);
1143      if (checkResult.ignore) {
1144        console.log(`AVPlayer ${commandName} ignored.`);
1145        this.schedule();
1146        return;
1147      }
1148      if (checkResult.index_to_remove >= 0) {
1149        let removedCommand = this.commands.splice(checkResult.index_to_remove, 1);
1150        console.log(`AVPlayer ${JSON.stringify(removedCommand)} removed.`);
1151        return;
1152      }
1153      this.commands.push({ func: ()=>{
1154        console.info('AVPlayer.pause()');
1155        this.avPlayer?.pause();
1156      }, name: commandName});
1157      this.schedule();
1158    }
1159    release() {
1160      this.commands.push({ func: ()=>{
1161        console.info('AVPlayer.release()');
1162        this.avPlayer?.release();
1163      }});
1164      this.schedule();
1165    }
1166    seek(time: number) {
1167      this.commands.push({ func: ()=>{
1168        console.info(`AVPlayer.seek(${time})`);
1169        this.avPlayer?.seek(time * 1000);
1170      }});
1171      this.schedule();
1172    }
1173    setVolume(volume: number) {
1174      this.commands.push({ func: ()=>{
1175        console.info(`AVPlayer.setVolume(${volume})`);
1176        this.avPlayer?.setVolume(volume);
1177      }});
1178      this.schedule();
1179    }
1180    setPlaybackRate(playbackRate: number) {
1181      let speed = media.PlaybackSpeed.SPEED_FORWARD_1_00_X;
1182      let delta = 0.05;
1183      playbackRate += delta;
1184      if (playbackRate < 1) {
1185        speed = media.PlaybackSpeed.SPEED_FORWARD_0_75_X;
1186      } else if (playbackRate < 1.25) {
1187        speed = media.PlaybackSpeed.SPEED_FORWARD_1_00_X;
1188      } else if (playbackRate < 1.5) {
1189        speed = media.PlaybackSpeed.SPEED_FORWARD_1_25_X;
1190      } else if (playbackRate < 2) {
1191        speed = media.PlaybackSpeed.SPEED_FORWARD_1_75_X;
1192      } else {
1193        speed = media.PlaybackSpeed.SPEED_FORWARD_2_00_X;
1194      }
1195      this.commands.push({ func: ()=>{
1196        console.info(`AVPlayer.setSpeed(${speed})`);
1197        this.avPlayer?.setSpeed(speed);
1198      }});
1199      this.schedule();
1200    }
1201  }
1202  ```
1203
1204- 前端页面示例。
1205
1206  ```html
1207  <html>
1208  <head>
1209      <title>视频托管测试html</title>
1210      <meta name="viewport" content="width=device-width">
1211  </head>
1212  <body>
1213  <div>
1214      <!-- 使用时需要自行替换视频链接 -->
1215      <video src='https://xxx.xxx/demo.mp4' style='width: 100%'></video>
1216  </div>
1217  </body>
1218  </html>
1219  ```
1220