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