鸿蒙ArkTS 视频相关 视频播放、直播视频、XComponent和typeNode多方案实现画中画功能开发

一、简单的视频播放、直播播放

1. 使用meida中的avPlayer结合XComponent进行视频播放

如果是音频只需要一个路径就差不多了,这是音频HDI+显示HDI,所以需要做以下几点:

    1. 应用从XComponent组件获取窗口SurfaceID,获取方式参考XComponent
    1. 应用把媒体资源、SurfaceID传递给AVPlayer接口。
    1. Player Framework把视频ES数据流输出给解码HDI,解码获得视频帧(NV12/NV21/RGBA)。
    1. Player Framework把音频PCM数据流输出给Audio Framework,Audio Framework输出给音频HDI。
    1. Player Framework把视频帧(NV12/NV21/RGBA)输出给Graphic Framework,Graphic Framework输出给显示HDI。
scss 复制代码
let mXComponentController: XComponentController = new XComponentController(); 

@Entry
...

avPlayer: media.AVPlayer = Object()
//xcomponentController: XComponentController = new XComponentController()
url: string =
  "https://vdept3.bdstatic.com/mda-rb9vxh2uiq5jq71p/cae_h264/1739221710340328239/mda-rb9vxh2uiq5jq71p.mp4?v_from_s=hkapp-haokan-nanjing&auth_key=1743244119-0-0-7e704e3b242ea6b12d1825943add20cb&bcevod_channel=searchbox_feed&cr=0&cd=0&pd=1&pt=3&logid=1719267439&vid=8058861658488074906&klogid=1719267439&abtest="


//media.createAVPlayer()
async playStart() {
  let avPlayer = await media.createAVPlayer()
  this.avPlayer = avPlayer
  avPlayer.on('stateChange', (state) => {
    if (state == 'initialized') {
      const id = mXComponentController.getXComponentSurfaceId()
      avPlayer.surfaceId = id
      //this.xcomponentController.setXComponentSurfaceRect({surfaceWidth: avPlayer.width, surfaceHeight: avPlayer.height})
      avPlayer.prepare()
    }
    if (state == 'prepared') {
      avPlayer.loop = true
      avPlayer.play()
    }
  })
  avPlayer.url = this.url
  //avPlayer.url='https://vdept3.bdstatic.com/mda-rbm39nfss0emhmzs/cae_h264/1740191336755572797/mda-rbm39nfss0emhmzs.mp4?v_from_s=hkapp-haokan-nanjing&auth_key=1743241772-0-0-501f03b3ade825e7629e56a8326832aa&bcevod_channel=searchbox_feed&pd=1&cr=0&cd=0&pt=3&logid=2972294090&vid=12649103641022590803&klogid=2972294090&abtest='
}


aboutToAppear(): void {
  this.playStart()
}

aboutToDisappear(): void {
  this.avPlayer.stop()
  this.avPlayer.release()
}

async pipShow(){
  pipController = await PiPWindow.create(config)
  pipController.startPiP()
  pipController.setAutoStartEnabled(true)
}

 build() {
    NavDestination() {
      Column() {
        MkNavbar({
          title: '视频',
          leftClickHandler: () => {
            // 点击返回
            this.pageStack.pop()
          }
        })
        Column({ space: 8 }) {
          // Button('开始播放')
          //   .onClick(() => {
          //     this.playStart()
          //   })
          Button('暂停播放')
            .onClick(() => {
              this.avPlayer.pause()
            })
          Button('继续播放')
            .onClick(() => {
              this.avPlayer.play()
            })
          XComponent({ type: XComponentType.SURFACE, controller: mXComponentController })
            .width('100%')
            .height(300)
          Button('随机播放视频')
            .onClick(async () => {
              this.url = this.list[Math.floor(Math.random() * this.list.length)]
              await this.avPlayer.reset()
              this.playStart()
            })
        }
      }
      .width('100%')
      .height('100%')
      .backgroundColor($r('[basic].color.under'))
    }
    .hideTitleBar(true)
    .id('gtxy4869')
  }
}

2. 使用Video组件做一个直播播放,上下滑动切换直播

① 主要了解一下VideoOptions里面配置的参数作用

  • src:Resource格式可以跨包/跨模块访问资源文件,常用于访问本地视频。支持rawfile文件下的资源,即通过$rawfile引用视频文件。string格式可用于加载网络视频和本地视频,常用于加载网络视频。支持网络视频地址。支持file://路径前缀的字符串,即应用沙箱URI:file:///。用于读取应用沙箱路径内的资源。需要保证目录包路径下的文件有可读权限。

  • currentProgressRate:视频播放倍速。说明:number取值仅支持:0.75,1.0,1.25,1.75,2.0。

  • previewUri:视频未播放时的预览图片路径,默认不显示图片。

  • controller:设置视频控制器,可以控制视频的播放状态。(重点)类型为:VideoController

VideoController上的方法可以控制我们的video
  • start() 开始播放。

  • pause() 暂停播放,显示当前帧,再次播放时从当前位置继续播放。

  • stop() 停止播放,显示当前帧,再次播放时从头开始播放。

  • reset(): void video组件重置AVPlayer。显示当前帧,再次播放时从头开始播放。

  • setCurrentTime(value: number) 指定视频播放的进度位置。

  • requestFullscreen(value: boolean) 请求全屏播放。

  • exitFullscreen() 退出全屏播放。

  • setCurrentTime(value: number, seekMode: SeekMode) 指定视频播放的进度位置,并指定跳转模式。

kotlin 复制代码
import { http } from '@kit.NetworkKit';

let httpRequest = http.createHttp();

function Live() {
  return httpRequest.request(
    'http://123.60.114.86:8090/goods/liveInfo?id=2',
    {
      header: {
        'Content-Type': 'application/json'
      },
      readTimeout: 60000,
      connectTimeout: 60000
    });
}

class LiveInfoResponse {
  code: number = 0;
  data: Array<Info> = [];
}

class Info {
  uri: string = '';
  name: string = '';
  peopleNum: string = '';
}
interface ILiveInfoDataModel {
  uri: string | Resource;
  name: string | Resource;
  peopleNum: string | Resource;
}

class LiveInfoDataModel {
  public uri: string | Resource;
  public name: string | Resource;
  public peopleNum: string | Resource;

  constructor(item: ILiveInfoDataModel) {
    this.uri = item.uri;
    this.name = item.name;
    this.peopleNum = item.peopleNum;
  }
}
const LiveData: Array<ILiveInfoDataModel> = [
  {
    uri: $rawfile('video1.mp4'),
    name: $r('app.string.first_author'),
    peopleNum: '520'
  },

  {
    uri: $rawfile('video2.mp4'),
    name: $r('app.string.two_author'),
    peopleNum: '360'
  },
  {
    uri: $rawfile('video3.mp4'),
    name: $r('app.string.three_author'),
    peopleNum: '777'
  }
];
@Entry
@Component
struct PageDouyin {
  @State active: number = 0;
  @State liveInfoList: Array<LiveInfoDataModel> = [];
  @State mData: LiveInfoResponse = {
    code: 0,
    data: [{ uri: '', name: '', peopleNum: '' }]
  }
  @State isPhone: boolean = false;
  portraitFunc: Callback<mediaquery.MediaQueryResult> = (mediaQueryResult: mediaquery.MediaQueryResult): void => {
    this.onPortrait(mediaQueryResult);
  };
  listenerIsPhone = mediaquery.matchMediaSync('(orientation:landscape)');

  //屏幕方向监听
  onPortrait(mediaQueryResult: mediaquery.MediaQueryResult) {
    this.isPhone = !mediaQueryResult.matches;
  }

  async aboutToAppear() {
    LiveData.forEach((item) => {
      this.liveInfoList.push(new LiveInfoDataModel(item));
    })
    this.listenerIsPhone.on('change', this.portraitFunc);

    try {
      let a = await Live();
      this.mData = JSON.parse(a.result.toString());
      this.liveInfoList = this.mData.data;
    } catch (error) {
      console.log('http resquest is fail:' + error);
    }
  }

  build() {
    Scroll() {
      Column() {
        Swiper() {
          ForEach(this.liveInfoList, (item: LiveInfoDataModel, index) => {
            Stack() {
              if (this.active === index) {
                Video({ src: item.uri })
                  .autoPlay(true)
                  .loop(false)
                  .controls(false)
                  .objectFit(ImageFit.Contain)
                  .width('100%')
                  .height('100%')
              }

              Row() {
                Row() {
                  Row() {
                    Image($r('app.media.live_author'))
                      .width(38)
                      .height(38)

                    Column() {
                      Text(item.name)
                        .fontSize(16)
                        .fontColor('#ffffff')
                      Row() {
                        Text(item.peopleNum)
                          .id(item.peopleNum as string)
                          .fontSize(12)
                          .fontColor('#ffffff')
                        Text($r('app.string.watch'))
                          .fontSize(12)
                          .fontColor('#ffffff')
                      }
                    }
                    .alignItems(HorizontalAlign.Start)
                    .padding({ left: '2%' })
                  }

                  Button($r('app.string.follow'))
                    .backgroundColor(Color.Red)
                    .height(35)
                    .width(70)
                }
                .justifyContent(FlexAlign.SpaceBetween)
                .padding({ left: this.isPhone ? '2%' : '1%', right: this.isPhone ? '2%' : '1%' })
                .width(this.isPhone ? '57.2%' : '30%')
                .aspectRatio(this.isPhone ? 5.15 : 7)
                .backgroundColor('rgba(0,0,0,0.40)')
                .borderRadius(this.isPhone ? 26 : 36)

                Column() {
                  Image($r('app.media.live_share'))
                    .width(42)
                    .height(42)
                }
                .margin({ left: this.isPhone ? '12%' : '49%' })

                Column() {
                  Image($r('app.media.live_close'))
                    .id('close')
                    .width(42)
                    .height(42)
                    .onClick(() => {
                      router.back();
                    })
                }
                .margin({ left: '4%' })
              }
              .position({ x: '2%', y: '5.1%' })
               
              // Column() {
              //   CommentPage() 
              // }
              // .position({ x: '2%', y: this.isPhone ? '72%' : '62%' })
            }
            .backgroundColor('#D8D8D8')
            .width('100%')
            .height('100%')
          })
        }
        .width('100%')
        .height('100%')
        .loop(false)
        .indicator(false)
        .vertical(true) //设置 Swiper 为垂直方向滑动,配合响应式布局,使得视频在不同屏幕方向下都能正确展示。
        .onChange((index: number) => {
          this.active = index;
        })
      }
    }
  }
}

二、多方案实现画中画功能开发

1. 使用XComponent实现画中画功能开发

  • 这里我们直接拉了官方的代码,他导入的AVPlayerDemo其实就是我们上面的AVPlayer,我们给他实现一下
  • PiPWindow.create(config)拿到控制器
  • ② 通过画中画控制器实例的tAutoStartEnabled口设置是否需要在应用返回桌面时自动启动画中画。
  • ③ 通过画中画控制器实例的on('stateChange')接口注册生命周期事件回调。
  • ④ 通过画中画控制器实例的on('controlPanelActionEvent')接口注册控制事件回调。
  • ⑤ 通过startPiP()口启动画中画。
typescript 复制代码
// 该页面用于展示画中画功能的基本使用
//import { AVPlayerDemo } from './AVPlayerDemo'; // 请自行实现视频播放的相关开发
import { BuilderNode, FrameNode, NodeController, Size, UIContext, PiPWindow } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
import { media } from '@kit.MediaKit';
import { MkNavbar } from 'basic';

const TAG = 'PIPView';

class Params {
  text: string = '';
  constructor(text: string) {
    this.text = text;
  }
}

// 开发者可以通过@Builder装饰器实现布局构建
@Builder
function buildText(params: Params) {
  Column() {
    Text(params.text)
      .fontSize(20)
      .fontColor(Color.Red)
  }
  .width('100%') // 宽度方向充满画中画窗口
  .height('100%') // 高度方向充满画中画窗口
}

// 开发者可通过继承NodeController实现自定义UI控制器
class TextNodeController extends NodeController {
  private message: string;
  private textNode: BuilderNode<[Params]> | null = null;
  constructor(message: string) {
    super();
    this.message = message;
  }

  // 通过BuilderNode加载自定义布局
  makeNode(context: UIContext): FrameNode | null {
    this.textNode = new BuilderNode(context);
    this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.message));
    return this.textNode.getFrameNode();
  }

  // 开发者可自定义该方法实现布局更新
  update(message: string) {
    console.log(`update message: ${message}`);
    if (this.textNode !== null) {
      this.textNode.update(new Params(message));
    }
  }
}

@Builder
function PIPViewBuilder() {
  PIPView()
}

@Component
export struct PIPView {
  @Consume pageStack: NavPathStack
  private surfaceId: string = ''; // surfaceId,用于关联XComponent与视频播放器
  private mXComponentController: XComponentController = new XComponentController();
  //private player?: AVPlayerDemo = undefined;
  private pipController?: PiPWindow.PiPController = undefined;
  private nodeController: TextNodeController = new TextNodeController('你看我屌吗');
  navId: string = '';
  avPlayer: media.AVPlayer = Object()
  url: string = "https://vdept3.bdstatic.com/mda-rb9vxh2uiq5jq71p/cae_h264/1739221710340328239/mda-rb9vxh2uiq5jq71p.mp4?v_from_s=hkapp-haokan-nanjing&auth_key=1743309662-0-0-33813516e090597e2933bb263907310a&bcevod_channel=searchbox_feed&cr=0&cd=0&pd=1&pt=3&logid=2462875327&vid=8058861658488074906&klogid=2462875327&abtest="

    //"https://vdept3.bdstatic.com/mda-rb9vxh2uiq5jq71p/cae_h264/1739221710340328239/mda-rb9vxh2uiq5jq71p.mp4?v_from_s=hkapp-haokan-nanjing&auth_key=1743244119-0-0-7e704e3b242ea6b12d1825943add20cb&bcevod_channel=searchbox_feed&cr=0&cd=0&pd=1&pt=3&logid=1719267439&vid=8058861658488074906&klogid=1719267439&abtest="

  async avPlay(id:string){
    let avPlayer = await media.createAVPlayer()
    this.avPlayer = avPlayer
    avPlayer.on('stateChange',(state)=>{
      if (state == 'initialized') {
        avPlayer.surfaceId = id
        avPlayer.prepare()
      }
      if (state == 'prepared') {
        avPlayer.loop = true
        avPlayer.play()
      }
    })
    avPlayer.url = this.url
  }


  build() {
    NavDestination() {
      Column() {
        MkNavbar({
          title: '画中画',
          leftClickHandler: () => {
            // 点击返回
            this.pageStack.pop()
          }
        })
        // XComponent控件,用于播放视频流
        XComponent({ id: 'pipDemo', type: 'surface', controller: this.mXComponentController })
          .onLoad(() => {
            this.surfaceId = this.mXComponentController.getXComponentSurfaceId();
            // 需要设置AVPlayer的surfaceId为XComponentController的surfaceId
            this.avPlay(this.surfaceId)
            //this.player = new AVPlayerDemo(this.surfaceId);
            //this.player.avPlayerFdSrcDemo();
          })
          .onDestroy(() => {
            console.info(`[${TAG}] XComponent onDestroy`);
          })
          .size({ width: '100%', height: '800px' })
        Row({ space: 20 }) {
          Button('start') // 启动画中画
            .onClick(() => {
              this.startPip();
            })
            .stateStyles({
              pressed: {
                .backgroundColor(Color.Red);
              },
              normal: {
                .backgroundColor(Color.Blue);
              }
            })
          Button('stop') // 停止画中画
            .onClick(() => {
              this.stopPip();
              this.avPlayer.stop()
              this.avPlayer.release()
            })
            .stateStyles({
              pressed: {
                .backgroundColor(Color.Red);
              },
              normal: {
                .backgroundColor(Color.Blue);
              }
            })
          Button('updateSize') // 更新视频尺寸
            .onClick(() => {
              // 此处设置的宽高应为媒体内容宽高,需要通过媒体相关接口或回调获取
              // 例如使用AVPlayer播放视频时,可通过videoSizeChange回调获取媒体源更新后的尺寸
              this.updateContentSize(900, 1600);
            })
            .stateStyles({
              pressed: {
                .backgroundColor(Color.Red);
              },
              normal: {
                .backgroundColor(Color.Blue);
              }
            })
        }
        .size({ width: '100%', height: 60 })
        .justifyContent(FlexAlign.SpaceAround)
      }
      .justifyContent(FlexAlign.Center)
      .height('100%')
      .width('100%')
    }
    .hideTitleBar(true)
  }

  startPip() {
    if (!PiPWindow.isPiPEnabled()) {
      console.error(`picture in picture disabled for current OS`);
      return;
    }
    let config: PiPWindow.PiPConfiguration = {
      context: getContext(this),
      componentController: this.mXComponentController,
      // 当前page导航id
      // 1、UIAbility使用Navigation管理页面,需要设置Navigation控件的id属性,并将该id设置给画中画控制器,确保还原场景下能够从画中画窗口恢复到原页面
      // 2、UIAbility使用Router管理页面时(画中画场景不推荐该导航方式),无需设置navigationId。注意:该场景下启动画中画后,不要进行页面切换,否则还原场景可能出现异常
      // 3、UIAbility只有单页面时,无需设置navigationId,还原场景下也能够从画中画窗口恢复到原页面
      navigationId: this.navId,
      templateType: PiPWindow.PiPTemplateType.VIDEO_PLAY, // 对于视频通话、视频会议等场景,需要设置相应的模板类型
      contentWidth: 1920, // 可选,创建画中画控制器时系统可通过XComponent组件大小设置画中画窗口比例
      contentHeight: 1080, // 可选,创建画中画控制器时系统可通过XComponent组件大小设置画中画窗口比例
      controlGroups:[PiPWindow.VideoPlayControlGroup.VIDEO_PREVIOUS_NEXT], // 可选,对于视频通话、视频会议和视频直播场景,可通过该属性选择对应模板类型下需显示的的控件组
      customUIController: this.nodeController, // 可选,如果需要在画中画显示内容上方展示自定义UI,可设置该参数。
    };
    // 步骤1:创建画中画控制器,通过create接口创建画中画控制器实例
    let promise : Promise<PiPWindow.PiPController> = PiPWindow.create(config);
    promise.then((controller : PiPWindow.PiPController) => {
      this.pipController = controller;
      // 步骤1:初始化画中画控制器
      this.initPipController();
      // 步骤2:通过startPiP接口启动画中画
      this.pipController.startPiP().then(() => {
        console.info(`Succeeded in starting pip.`);
      }).catch((err: BusinessError) => {
        console.error(`Failed to start pip. Cause:${err.code}, message:${err.message}`);
      });
    }).catch((err: BusinessError) => {
      console.error(`Failed to create pip controller. Cause:${err.code}, message:${err.message}`);
    });
  }

  initPipController() {
    if (!this.pipController) {
      return;
    }
    // 步骤1:通过setAutoStartEnabled接口设置是否需要在应用返回桌面时自动启动画中画,注册stateChange和controlPanelActionEvent回调
    this.pipController.setAutoStartEnabled(false /*or true if necessary*/); // 默认为false
    this.pipController.on('stateChange', (state: PiPWindow.PiPState, reason: string) => {
      this.onStateChange(state, reason);
    });
    this.pipController.on('controlPanelActionEvent', (event: PiPWindow.PiPActionEventType, status?: number) => {
      this.onActionEvent(event, status);
    });
  }

  onStateChange(state: PiPWindow.PiPState, reason: string) {
    let curState: string = '';
    switch(state) {
      case PiPWindow.PiPState.ABOUT_TO_START:
        curState = "ABOUT_TO_START";
        break;
      case PiPWindow.PiPState.STARTED:
        curState = "STARTED";
        break;
      case PiPWindow.PiPState.ABOUT_TO_STOP:
        curState = "ABOUT_TO_STOP";
        break;
      case PiPWindow.PiPState.STOPPED:
        curState = "STOPPED";
        break;
      case PiPWindow.PiPState.ABOUT_TO_RESTORE:
        curState = "ABOUT_TO_RESTORE";
        break;
      case PiPWindow.PiPState.ERROR:
        curState = "ERROR";
        break;
      default:
        break;
    }
    console.info(`[${TAG}] onStateChange: ${curState}, reason: ${reason}`);
  }

  onActionEvent(event: PiPWindow.PiPActionEventType, status?: number) {
    switch (event) {
      case 'playbackStateChanged':
        // 开始或停止视频
        if (status === 0) {
          // 停止视频
        } else if (status === 1) {
          // 播放视频
        }
        break;
      case 'nextVideo':
        // 播放上一个视频
        break;
      case 'previousVideo':
        // 播放下一个视频
        break;
      default:
        break;
    }
  }

  // 步骤3:视频内容变化时,向画中画控制器更新视频尺寸信息,用于调整画中画窗口比例
  updateContentSize(width: number, height: number) {
    if (this.pipController) {
      this.pipController.updateContentSize(width, height);
    }
  }

  // 步骤4:当不再需要显示画中画时,通过stopPiP接口关闭画中画
  stopPip() {
    if (this.pipController) {
      let promise : Promise<void> = this.pipController.stopPiP();
      promise.then(() => {
        console.info(`Succeeded in stopping pip.`);
        this.pipController?.off('stateChange'); // 如果已注册stateChange回调,停止画中画时取消注册该回调
        this.pipController?.off('controlPanelActionEvent'); // 如果已注册controlPanelActionEvent回调,停止画中画时取消注册该回调
      }).catch((err: BusinessError) => {
        console.error(`Failed to stop pip. Cause:${err.code}, message:${err.message}`);
      });
    }
  }
}

2. 用typeNode实现画中画功能开发

不同场景下画中画控制层的不同呈现:
  • 在使用create接口创建画中画时,可通过在PiPConfiguration中新增PiPControlGroup类型的数组配置当前画中画控制层控件。
  • 比如VideoCallControlGroup视频通话控件组枚举。然后配置PiPTemplateTypeVIDEO_CALL
我们先封装一个PipManager(管理器)
typescript 复制代码
    // model/PipManager.ets
import { PiPWindow, typeNode } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
import { XCNodeController } from './XCNodeController';
import { AVPlayer } from './AVPlayer'

export class CustomXComponentController extends XComponentController {
  onSurfaceCreated(surfaceId: string): void {
    console.log(TAG, `onSurfaceCreated surfaceId: ${surfaceId}`);
    if (PipManager.getInstance().player.surfaceID === surfaceId) {
      return;
    }
    // 将surfaceId设置给媒体源
    PipManager.getInstance().player.surfaceID = surfaceId;
    PipManager.getInstance().player.avPlayerFdSrc();
  }

  onSurfaceDestroyed(surfaceId: string): void {
    console.log(TAG, `onSurfaceDestroyed surfaceId: ${surfaceId}`);
  }
}

const TAG = 'PipManager';

export class PipManager {
  private static instance: PipManager = new PipManager();
  private pipController?: PiPWindow.PiPController = undefined;
  private xcNodeController: XCNodeController;
  private mXComponentController: XComponentController;
  private lifeCycleCallback: Set<Function> = new Set();
  player: AVPlayer;

  public static getInstance(): PipManager {
    return PipManager.instance;
  }

  constructor() {
    this.xcNodeController = new XCNodeController();
    this.player = new AVPlayer();
    this.mXComponentController = new CustomXComponentController();
  }

  public registerLifecycleCallback(callBack: Function) {
    this.lifeCycleCallback.add(callBack);
  }

  public unRegisterLifecycleCallback(callBack: Function): void {
    this.lifeCycleCallback.delete(callBack);
  }

  getNode(): typeNode.XComponent | null {
    return this.xcNodeController.getNode();
  }

  onActionEvent(control: PiPWindow.ControlEventParam) {
    switch (control.controlType) {
      case PiPWindow.PiPControlType.VIDEO_PLAY_PAUSE:
        if (control.status === PiPWindow.PiPControlStatus.PAUSE) {
          //停止视频
        } else if (control.status === PiPWindow.PiPControlStatus.PLAY) {
          //播放视频
        }
        break;
      case PiPWindow.PiPControlType.VIDEO_NEXT:
        // 切换到下一个视频
        break;
      case PiPWindow.PiPControlType.VIDEO_PREVIOUS:
        // 切换到上一个视频
        break;
      case PiPWindow.PiPControlType.FAST_FORWARD:
        // 视频进度快进
        break;
      case PiPWindow.PiPControlType.FAST_BACKWARD:
        // 视频进度后退
        break;
      default:
        break;
    }
    console.info('onActionEvent, controlType:' + control.controlType + ', status' + control.status);
  }

  onStateChange(state: PiPWindow.PiPState, reason: string) {
    let curState: string = '';
    this.xcNodeController.setCanAddNode(
      state === PiPWindow.PiPState.ABOUT_TO_STOP || state === PiPWindow.PiPState.STOPPED)
    if (this.lifeCycleCallback !== null) {
      this.lifeCycleCallback.forEach((fun) => {
        fun(state);
      });
    }
    switch (state) {
      case PiPWindow.PiPState.ABOUT_TO_START:
        curState = "ABOUT_TO_START";
        // 将typeNode节点从布局移除
        this.xcNodeController.removeNode();
        break;
      case PiPWindow.PiPState.STARTED:
        curState = "STARTED";
        break;
      case PiPWindow.PiPState.ABOUT_TO_STOP:
        curState = "ABOUT_TO_STOP";
        break;
      case PiPWindow.PiPState.STOPPED:
        curState = "STOPPED";
        break;
      case PiPWindow.PiPState.ABOUT_TO_RESTORE:
        curState = "ABOUT_TO_RESTORE";
        break;
      case PiPWindow.PiPState.ERROR:
        curState = "ERROR";
        break;
      default:
        break;
    }
    console.info(`[${TAG}] onStateChange: ${curState}, reason: ${reason}`);
  }

  unregisterPipStateChangeListener() {
    console.info(`${TAG} aboutToDisappear`);
    this.pipController?.off('stateChange');
    this.pipController?.off('controlEvent');
  }

  getXComponentController(): CustomXComponentController {
    return this.mXComponentController;
  }

  // 步骤1:创建画中画控制器,注册生命周期事件以及控制事件回调
  init(ctx: Context) {
    if (this.pipController !== null && this.pipController != undefined) {
      return;
    }
    console.info(`${TAG} onPageShow`)
    if (!PiPWindow.isPiPEnabled()) {
      console.error(TAG, `picture in picture disabled for current OS`);
      return;
    }

    let config: PiPWindow.PiPConfiguration = {
      context: ctx,
      componentController: this.getXComponentController(),
      templateType: PiPWindow.PiPTemplateType.VIDEO_PLAY,
      contentWidth: 1920, // 使用typeNode启动画中画时,contentWidth需设置为大于0的值,否则创建画中画失败
      contentHeight: 1080, // 使用typeNode启动画中画时,contentHeight需设置为大于0的值,否则创建画中画失败
    };
    // 通过create接口创建画中画控制器实例
    let promise: Promise<PiPWindow.PiPController> = PiPWindow.create(config, this.xcNodeController.getNode());
    promise.then((controller: PiPWindow.PiPController) => {
      this.pipController = controller;
      // 通过画中画控制器实例的setAutoStartEnabled接口设置是否需要在应用返回桌面时自动启动画中画
      this.pipController?.setAutoStartEnabled(true);
      // 通过画中画控制器实例的on('stateChange')接口注册生命周期事件回调
      this.pipController.on('stateChange', (state: PiPWindow.PiPState, reason: string) => {
        this.onStateChange(state, reason);
      });
      // 通过画中画控制器实例的on('controlEvent')接口注册控制事件回调
      this.pipController.on('controlEvent', (control: PiPWindow.ControlEventParam) => {
        this.onActionEvent(control);
      });
    }).catch((err: BusinessError) => {
      console.error(TAG, `Failed to create pip controller. Cause:${err.code}, message:${err.message}`);
    });
  }

  // 步骤2:启动画中画
  startPip() {
    this.pipController?.startPiP().then(() => {
      console.info(TAG, `Succeeded in starting pip.`);
    }).catch((err: BusinessError) => {
      console.error(TAG, `Failed to start pip. Cause:${err.code}, message:${err.message}`);
    });
  }

  // 步骤3:更新媒体源尺寸信息
  updateContentSize(width: number, height: number) {
    if (this.pipController) {
      this.pipController.updateContentSize(width, height);
    }
  }

  // 步骤4:关闭画中画
  stopPip() {
    if (this.pipController === null || this.pipController === undefined) {
      return;
    }
    let promise: Promise<void> = this.pipController.stopPiP();
    promise.then(() => {
      console.info(TAG, `Succeeded in stopping pip.`);
    }).catch((err: BusinessError) => {
      console.error(TAG, `Failed to stop pip. Cause:${err.code}, message:${err.message}`);
    });
  }

  getNodeController(): XCNodeController {
    console.info(TAG, `getNodeController.`);
    return this.xcNodeController;
  }

  setAutoStart(autoStart: boolean): void {
    this.pipController?.setAutoStartEnabled(autoStart);
  }

  removeNode() {
    this.xcNodeController.removeNode();
  }

  addNode(): void {
    this.xcNodeController.addNode();
  }
}
配置页面
scss 复制代码
    // pages/Page1.ets
import { PipManager } from '../model/PipManager';

const TAG = 'Page1';

@Component
export struct Page1 {
  build() {
    Column() {
      Text('This is Page1')
        .fontSize(30)
        .fontWeight(FontWeight.Bold)
        .margin({bottom: 20})

      // 将typeNode添加到页面布局中
      NodeContainer(PipManager.getInstance().getNodeController())
        .size({ width: '100%', height: '800px' })

      Row({ space: 20 }) {
        Button('startPip')// 启动画中画
          .onClick(() => {
            PipManager.getInstance().startPip();
          })

        Button('stopPip')// 停止画中画
          .onClick(() => {
            PipManager.getInstance().stopPip();
          })

        Button('updateSize')// 更新视频尺寸
          .onClick(() => {
            // 此处设置的宽高应为媒体内容宽高,需要通过媒体相关接口或回调获取
            // 例如使用AVPlayer播放视频时,可通过videoSizeChange回调获取媒体源更新后的尺寸
            PipManager.getInstance().updateContentSize(900, 1600);
          })
      }
      .backgroundColor('#4da99797')
      .size({ width: '100%', height: 60 })
      .justifyContent(FlexAlign.SpaceAround)
    }
    .justifyContent(FlexAlign.Center)
    .width('100%')
    .height('100%')
  }

  onPageShow(): void {
    console.info(TAG, 'onPageShow')
    PipManager.getInstance().initPipController(getContext(this));
    PipManager.getInstance().setAutoStart(true);
  }

  onPageHide(): void {
    console.info(TAG, 'onPageHide')
    PipManager.getInstance().setAutoStart(false);
    PipManager.getInstance().removeNode();
  }
}
相关推荐
秋叶先生_15 分钟前
HarmonyOS NEXT——【鸿蒙监听网络状态变化】
华为·harmonyos·鸿蒙
东林知识库34 分钟前
鸿蒙NEXT小游戏开发:围住神经猫
harmonyos
zacksleo37 分钟前
鸿蒙Flutter开发故事:不,你不需要鸿蒙化
flutter·harmonyos
chat2tomorrow2 小时前
数据仓库是什么?数据仓库的前世今生 (数据仓库系列一)
大数据·数据库·数据仓库·低代码·华为·spark·sql2api
别说我什么都不会3 小时前
OpenHarmony解读之设备认证:sts协议-客户端发起sts end请求
物联网·嵌入式·harmonyos
悬空八只脚6 小时前
React-Native开发鸿蒙NEXT-本地与沙盒加载bundle
harmonyos
鸿蒙布道师6 小时前
鸿蒙NEXT开发日志工具类(ArkTs)
android·ios·华为·harmonyos·arkts·鸿蒙系统·huawei
90后的晨仔6 小时前
HarmonyOS的页面生命周期 和 组件生命周期
harmonyos
桃子酱紫君16 小时前
华为配置篇-ISIS基础实验
华为
泡泡大魔王17 小时前
鸿蒙ArkTS开发:微信/系统来电通话监听功能实现
华为·harmonyos