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 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.ets。 940 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