1# 托管网页中的媒体播放 2 3Web组件提供了应用接管网页中媒体播放的能力,用来支持应用增强网页的媒体播放,如画质增强等。 4 5## 使用场景 6 7网页播放媒体时,存在着网页视频不够清晰、网页的播放器界面简陋功能少、一些视频不能播放的问题。 8 9此时,应用开发者可以使用该功能,通过自己或者第三方的播放器,接管网页媒体播放来改善网页的媒体播放体验。 10 11## 实现原理 12 13### ArkWeb内核播放媒体的框架 14 15不开启该功能时,ArkWeb内核的播放架构如下所示: 16 17  18 19 > **说明:** 20 > 21 > - 上图中1表示ArkWeb内核创建WebMediaPlayer来播放网页中的媒体资源。 22 > - 上图中2表示WebMediaPlayer使用系统解码器来渲染媒体数据。 23 24开启该功能后,ArkWeb内核的播放架构如下: 25 26  27 28 > **说明:** 29 > 30 > - 上图中1表示ArkWeb内核创建WebMediaPlayer来播放网页中的媒体资源。 31 > - 上图中2表示WebMediaPlayer使用应用提供的本地播放器(NativeMediaPlayer)来渲染媒体数据。 32 33 34### ArkWeb内核与应用的交互 35 36  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.ets。 941 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