鸿蒙NEXT版实战开发:使用typeNode实现画中画功能开发

103 篇文章 0 订阅
67 篇文章 0 订阅

往期鸿蒙全套实战精彩文章必看内容:


使用typeNode实现画中画功能开发

 

本文以视频播放为例,介绍通过typeNode实现画中画功能的基本开发步骤。

示例中的视频播放器简易实现参考:

// model/AVPlayer.ets
// 简易播放器实现
import { BusinessError } from '@kit.BasicServicesKit';
import { common } from '@kit.AbilityKit';
import { media } from '@kit.MediaKit';

export class AVPlayer {
  private avPlayer?: media.AVPlayer;
  surfaceID: string = '';
  
  setAVPlayerCallback() {
    this.avPlayer?.on('seekDone', (seekDoneTime: number) => {
      console.info(`AVPlayer seek succeeded, seek time is ${seekDoneTime}`);
    })
    this.avPlayer?.on('stateChange', async (state, reason) => {
      if (!this.avPlayer) {
        return;
      }
      switch (state) {
        case 'idle':
          this.avPlayer.release();
          break;
        case 'initialized':
          this.avPlayer.surfaceId = this.surfaceID;
          this.avPlayer.prepare().then(() => {
            console.info('AVPlayer prepare succeeded.');
          }, (err: BusinessError) => {
            console.error(`Invoke prepare failed, code is ${err.code}, message is ${err.message}`);
          });
          break;
        case 'prepared':
          this.avPlayer.play();
          break;
        case 'stopped':
          this.avPlayer.reset();
          break;
        default:
          break;
      }
    })
  }

  async avPlayerFdSrc() {
    this.avPlayer = await media.createAVPlayer();
    this.setAVPlayerCallback();
    let context = getContext(this) as common.UIAbilityContext;
    let fileDescriptor = await context.resourceManager.getRawFd('xxx.mp4');
    this.avPlayer.fdSrc = fileDescriptor;
  }
}

约束与限制

  • 构造PiPConfiguration参数时,建议传入contentWidth和contentHeight参数用以计算画中画初始比例,否则系统将以16:9的比例呈现画中画窗口。
  • contentNode支持XComponentType.SURFACE类型,且创建typeNode时必须指定为"XComponent"类型。

应用使用typeNode自由节点(不添加到布局)实现画中画功能

  1. 创建画中画控制器,注册生命周期事件以及控制事件回调。

    • 通过主窗口UIContext创建typeNode节点。
    • 通过create(config: PiPConfiguration, contentNode: typeNode.XComponent)接口创建画中画控制器实例。
    • 通过画中画控制器实例的setAutoStartEnabled接口设置是否需要在应用返回桌面时自动启动画中画。
    • 通过画中画控制器实例的on('stateChange')接口注册生命周期事件回调。
    • 通过画中画控制器实例的on('controlEvent')接口注册控制事件回调。

  2. 启动画中画。

    创建画中画控制器实例后,通过startPiP接口启动画中画。

  3. 更新媒体源尺寸信息。

    画中画媒体源更新后(如切换视频),通过画中画控制器实例的updateContentSize接口更新媒体源尺寸信息,以调整画中画窗口比例。

  4. 关闭画中画。

    当不再需要显示画中画时,可根据业务需要,通过画中画控制器实例的stopPiP接口关闭画中画。

// EntryAbility.ets
import { UIAbility } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { PipManager } from '../model/PipManager';

export default class EntryAbility extends UIAbility {
  onWindowStageCreate(windowStage: window.WindowStage): void {
    windowStage.loadContent('pages/Index', (err) => {
      // ...
    });
    windowStage.getMainWindow().then((window) => {
      let ctx = window.getUIContext();
      // 通过主窗口UIContext创建typeNode节点
      PipManager.getInstance().makeTypeNode(ctx);
    })
  }
}
// Index.ets
// 该页面用于展示应用布局文件,创建的typeNode节点不会添加到该布局中
import { PipManager } from '../model/PipManager';

const TAG = 'Index'
@Entry
@Component
struct Index {
  build() {
    Column() {
      Text('This is MainPage')
        .fontSize(30)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 20 })

      Text('This is not typeNode')
        .size({ width: '100%', height: '800px' })
        .fontSize(30)
        .textAlign(TextAlign.Center)
        .fontWeight(FontWeight.Bold)
        .backgroundColor('#4d5b5858')

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

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

        Button('updateSize') // 更新视频尺寸
          .onClick(() => {
            PipManager.getInstance().updateContentSize(900, 1600);
          })
      }
      .backgroundColor('#4da99797')
      .size({ width: '100%', height: 60 })
      .justifyContent(FlexAlign.SpaceAround)
    }
    .justifyContent(FlexAlign.Center)
    .width('100%')
    .height('100%')
  }

  aboutToDisappear(): void {
    PipManager.getInstance().unregisterPipStateChangeListener(); // 解注册画中画生命周期及状态回调
  }

  onPageShow(): void {
    console.info(TAG, 'onPageShow')
    PipManager.getInstance().init(getContext(this)); // 创建画中画控制器
    PipManager.getInstance().setAutoStart(true); // 设置应用退后台时自动启动画中画
  }

  onPageHide(): void {
    console.info(TAG, 'onPageHide')
    PipManager.getInstance().setAutoStart(false);
  }
}
// model/PipManager.ets
// 画中画控制器单例
import { PiPWindow, typeNode } from '@kit.ArkUI'; // 引入PiPWindow模块
import { BusinessError } from '@kit.BasicServicesKit';
import { AVPlayer} from '../model/AVPlayer'

// 自定义XComponentController
class CustomXComponentController extends XComponentController {
  // 监听onSurfaceCreated,并将surfaceId设置给播放器
  onSurfaceCreated(surfaceId: string): void {
    console.log(TAG, `onSurfaceCreated surfaceId: ${surfaceId}`);
    if (PipManager.getInstance().player.surfaceID === surfaceId) {
      return;
    }
    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 {
  player: AVPlayer;
  private static instance: PipManager = new PipManager();
  private pipController?: PiPWindow.PiPController = undefined;
  private mXComponentController: XComponentController;
  private xComponent: typeNode.XComponent| null = null; // typeNode节点

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

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

  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 = '';
    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}`);
  }

  // 解注册监听
  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的值,否则将设置为16:9默认比例
      contentHeight: 1080, // 使用typeNode启动画中画时,contentHeight需设置为大于0的值,否则将设置为16:9默认比例
    };
    // 通过create接口创建画中画控制器实例
    let promise: Promise<PiPWindow.PiPController> = PiPWindow.create(config, this.xComponent);
    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接口启动画中画
  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}`);
    });
  }

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

  // 创建typeNode节点
  makeTypeNode(ctx: UIContext) {
    if (this.xComponent === null || this.xComponent === undefined) {
      // 创建typeNode
      // let xc_options: XComponentOptions = {
      //   type: XComponentType.TEXTURE, // 类型设置为TEXTURE
      //   controller: PipManager.getInstance().getXComponentController(), // 设置XComponentController
      // }
      // this.xComponent = typeNode.createNode(ctx, "XComponent", xc_options);

      // 创建XComponent类型的typeNode
      this.xComponent = typeNode.createNode(ctx, "XComponent", {
        type: XComponentType.SURFACE, // 类型设置为SURFACE
        controller: PipManager.getInstance().getXComponentController(), // 设置XComponentController
      });
    }
  }
}

 以上示例代码对应的示意图如下所示:

应用使用router导航时通过typeNode实现画中画功能

  1. 创建画中画控制器,注册生命周期事件以及控制事件回调。

    • 创建自定义NodeController,实现makeNode方法,在该方法中创建typeNode。
    • 通过create(config: PiPConfiguration, contentNode: typeNode.XComponent)接口创建画中画控制器实例。
    • 通过画中画控制器实例的setAutoStartEnabled接口设置是否需要在应用返回桌面时自动启动画中画。
    • 通过画中画控制器实例的on('stateChange')接口注册生命周期事件回调。
    • 通过画中画控制器实例的on('controlEvent')接口注册控制事件回调。

  2. 启动画中画。

    创建画中画控制器实例后,通过startPiP接口启动画中画,在画中画ABOUT_TO_START生命周期将typeNode节点从布局移除,并返回上级界面(可选)。如果启动画中画时返回了上级界面,需要在画中画ABOUT_TO_RESTORE(还原)时重新push原界面。

  3. 更新媒体源尺寸信息。

    画中画媒体源更新后(如切换视频),通过画中画控制器实例的updateContentSize接口更新媒体源尺寸信息,以调整画中画窗口比例。

  4. 关闭画中画。

    当不再需要显示画中画时,可根据业务需要,通过画中画控制器实例的stopPiP接口关闭画中画,在画中画ABOUT_TO_STOP生命周期将typeNode节点重新添加到布局中。

// EntryAbility.ets
import { UIAbility } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';

export default class EntryAbility extends UIAbility {
  onWindowStageCreate(windowStage: window.WindowStage): void {
    windowStage.loadContent('pages/Index', (err) => {
     // ...
    });
  }
}
// pages/Index.ets
import { PipManager } from '../model/PipManager';
import { PiPWindow, router, Router } from '@kit.ArkUI'; // 引入PiPWindow模块

const TAG = 'Index'
@Entry
@Component
struct Index {
  private page1: string = 'pages/Page1';
  private pageRouter: Router | null = null;

  // 画中画生命周期事件监听,用于页面及节点操作
  private callback: Function = (state: PiPWindow.PiPState) => {
    console.info(TAG, `pipStateChange: state ${state}`);
    if (state === PiPWindow.PiPState.ABOUT_TO_START) {
      // 返回到上级页面(可选)
      this.pageRouter?.back();
    } else if (state === PiPWindow.PiPState.ABOUT_TO_STOP) {
      // 重新将typeNode节点添加到布局中,例如还原场景
      PipManager.getInstance().addNode();
    } else if (state === PiPWindow.PiPState.ABOUT_TO_RESTORE) {
      // 如果在ABOUT_TO_START时返回了上级界面,需要还原时push到原界面
      this.jumpNext();
    }
  };

  aboutToAppear(): void {
    this.pageRouter = this.getUIContext().getRouter();
    PipManager.getInstance().registerLifecycleCallback(this.callback);
  }

  aboutToDisappear(): void {
    PipManager.getInstance().unregisterPipStateChangeListener();
    PipManager.getInstance().unRegisterLifecycleCallback(this.callback);
  }

  jumpNext(): void {
    let topPage = this.pageRouter?.getState();
    if (topPage !== undefined && (this.page1.toString() === topPage.path + topPage.name)) {
      console.info(TAG, `page1 aready at top`)
      return;
    }
    this.pageRouter?.pushUrl({
      url: this.page1 // 目标url
    }, router.RouterMode.Standard, (err) => {
      if (err) {
        console.error(TAG, `Invoke pushUrl failed, code is ${err.code}: ${err.message}`);
        return;
      }
      console.info(TAG, 'Invoke pushUrl succeeded.');
    });
  }

  build() {
    Row() {
      Column() {
        Text('Main Page')
          .fontSize(50)
          .fontWeight(FontWeight.Bold)

        Button('Jump Next')
          .onClick(() => {
            this.jumpNext();
          })
          .margin({ top: 16, bottom: 16 })
      }
      .width('100%')
    }
    .height('100%')
  }
}
// pages/Page1.ets
import { PipManager } from '../model/PipManager';

const TAG = 'Page1';
@Entry
@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();
  }
}
// model/PipManager.ets
import { PiPWindow, typeNode } from '@kit.ArkUI'; // 引入PiPWindow模块
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:创建画中画控制器,注册生命周期事件以及控制事件回调
  initPipController(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.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) {
      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(): void {
    this.xcNodeController.removeNode();
  }

  addNode(): void {
    this.xcNodeController.addNode();
  }
}
// model/XCNodeController.ets
import { FrameNode, NodeController, typeNode } from '@kit.ArkUI';
import { PipManager } from './PipManager';

const TAG = 'XCNodeController';
// 创建自定义NodeController
export class XCNodeController extends NodeController {
  xComponent: typeNode.XComponent | null = null;
  private node: FrameNode | null = null;
  private canAddNode: boolean = true;

  // 设置是否可以添加节点
  setCanAddNode(canAddNode: boolean) {
    this.canAddNode = canAddNode;
  }

  // 实现makeNode方法,当自定义NodeController被添加到布局时,该方法会被调用
  makeNode(context: UIContext): FrameNode | null {
    this.node = new FrameNode(context);
    this.node.commonAttribute
    if (this.xComponent === null || this.xComponent === undefined) {
      // 创建typeNode
      // let xc_options: XComponentOptions = {
      //   type: XComponentType.TEXTURE, // 类型设置为TEXTURE
      //   controller: PipManager.getInstance().getXComponentController(), // 设置XComponentController
      // }
      // this.xComponent = typeNode.createNode(context, "XComponent", xc_options);

      // 创建XComponent类型的typeNode
      this.xComponent = typeNode.createNode(context, "XComponent", {
        type: XComponentType.SURFACE, // 类型设置为SURFACE
        controller: PipManager.getInstance().getXComponentController(), // 设置XComponentController
      });
    }
    if (this.canAddNode) {
      this.xComponent.getParent()?.removeChild(this.xComponent);
      this.node.appendChild(this.xComponent);
    }
    return this.node;
  }

  // 重新添加typeNode节点
  addNode() {
    if (this.node !== null && this.node !== undefined) {
      console.info(TAG, "addNode");
      this.node.appendChild(this.xComponent);
    }
  }

  // 移除typeNode节点
  removeNode() {
    if (this.node !== null && this.node !== undefined) {
      console.info(TAG, "removeNode");
      this.node.removeChild(this.xComponent);
    }
  }

  getNode(): typeNode.XComponent | null {
    console.info(TAG, "getNode is null:"+ (this.xComponent === null || this.xComponent === undefined))
    return this.xComponent;
  }
}

 以上示例代码对应的示意图如下所示:

应用使用Navigation导航时通过typeNode实现画中画功能

  1. 创建画中画控制器,注册生命周期事件以及控制事件回调。

    • 创建自定义NodeController,实现makeNode方法,在该方法中创建typeNode。
    • 通过create(config: PiPConfiguration, contentNode: typeNode.XComponent)接口创建画中画控制器实例。
    • 通过画中画控制器实例的setAutoStartEnabled接口设置是否需要在应用返回桌面时自动启动画中画。
    • 通过画中画控制器实例的on('stateChange')接口注册生命周期事件回调。
    • 通过画中画控制器实例的on('controlEvent')接口注册控制事件回调。

  2. 启动画中画。

    创建画中画控制器实例后,通过startPiP接口启动画中画,在画中画ABOUT_TO_START生命周期将typeNode节点从布局移除,并返回上级界面(可选)。如果启动画中画时返回了上级界面,需要在画中画ABOUT_TO_RESTORE(还原)时重新跳转到原界面。

  3. 更新媒体源尺寸信息。

    画中画媒体源更新后(如切换视频),通过画中画控制器实例的updateContentSize接口更新媒体源尺寸信息,以调整画中画窗口比例。

  4. 关闭画中画。

    当不再需要显示画中画时,可根据业务需要,通过画中画控制器实例的stopPiP接口关闭画中画,在画中画ABOUT_TO_STOP生命周期将typeNode节点重新添加到布局中。

// EntryAbility.ets
import { UIAbility } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';

export default class EntryAbility extends UIAbility {
  onWindowStageCreate(windowStage: window.WindowStage): void {
    windowStage.loadContent('pages/Index', (err) => {
      // ...
    });
  }
}
// pages/Index.ets
import { PipManager } from '../model/PipManager';
import { Page1 } from "../pages/Page1"
import { PiPWindow } from '@kit.ArkUI';

const TAG = 'Index1';

@Entry
@Component
struct Index {
  @Provide('pageInfos') pageInfos: NavPathStack = new NavPathStack();
  // 画中画生命周期事件监听,用于页面及节点操作
  private callback: Function = (state: PiPWindow.PiPState) => {
    console.info(TAG, `pipStateChange: state ${state}`);
    if (state === PiPWindow.PiPState.ABOUT_TO_START) {
      // 返回到上级页面(可选)
      this.pageInfos.pop();
    } else if (state === PiPWindow.PiPState.ABOUT_TO_STOP) {
      // 重新将typeNode节点添加到布局中,例如还原场景
      PipManager.getInstance().addNode();
    } else if (state === PiPWindow.PiPState.ABOUT_TO_RESTORE) {
      // 如果在ABOUT_TO_START时返回了上级界面,需要还原时push到原界面
      this.jumpNext();
    }
  };

  jumpNext() {
    if (this.pageInfos.getAllPathName()[0] === 'Page1') {
      console.log(TAG, 'Page1 already at top');
      return;
    }
    this.pageInfos.pushPath({ name: 'Page1' });
  }

  aboutToAppear(): void {
    PipManager.getInstance().registerLifecycleCallback(this.callback);
  }

  aboutToDisappear(): void {
    PipManager.getInstance().unregisterPipStateChangeListener();
    PipManager.getInstance().unRegisterLifecycleCallback(this.callback);
  }

  @Builder
  PageMap(name: string) {
    if (name === 'Page1') {
      Page1();
    }
  }

  build() {
    Navigation(this.pageInfos) {
      Column() {
        Text("This is Main Page")
        Column()
          .height('200px')
        Row({ space: 12 }) {
          Button("Jump Page1")
            .width('80%')
            .height(40)
            .margin(20)
            .onClick(() => {
              this.jumpNext();
            })
        }
      }
      .height("100%")
      .width("100%")
      .justifyContent(FlexAlign.Center)
      .backgroundColor("#DCDCDC")
    }.title('MainTitle')
    .navDestination(this.PageMap)
  }
}
// pages/Page1.ets
import { PipManager } from '../model/PipManager';

const TAG = 'Page1';

@Entry
@Component
export struct Page1 {
  build() {
    NavDestination() {
      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%')
    }
    .title('page1')
    .onShown(()=>{
      console.info(TAG, 'onShown')
      PipManager.getInstance().init(getContext(this));
      PipManager.getInstance().setAutoStart(true);
    })
    .onHidden(()=>{
      console.info(TAG, 'onHidden')
      PipManager.getInstance().setAutoStart(false);
      PipManager.getInstance().removeNode();
    })
  }
}
// model/XCNodeController.ets
import { FrameNode, NodeController, typeNode } from '@kit.ArkUI';
import { PipManager } from './PipManager';

const TAG = 'XCNodeController';

// 创建自定义NodeController
export class XCNodeController extends NodeController {
  xComponent: typeNode.XComponent| null = null;
  private node: FrameNode | null = null;
  private canAddNode: boolean = true;

  // 设置是否可以添加节点
  setCanAddNode(canAddNode: boolean) {
    this.canAddNode = canAddNode;
  }

  // 实现makeNode方法,当自定义NodeController被添加到布局时,该方法会被调用
  makeNode(context: UIContext): FrameNode | null {
    console.info(TAG, "makeNode");
    this.node = new FrameNode(context);
    if (this.xComponent === null || this.xComponent === undefined) {
      // 创建typeNode
      // let xc_options: XComponentOptions = {
      //   type: XComponentType.TEXTURE, // 类型设置为TEXTURE
      //   controller: PipManager.getInstance().getXComponentController(), // 设置XComponentController
      // }
      // this.xComponent = typeNode.createNode(context, "XComponent", xc_options);

      // 创建XComponent类型的typeNode
      this.xComponent = typeNode.createNode(context, "XComponent", {
        type: XComponentType.SURFACE, // 类型设置为SURFACE
        controller: PipManager.getInstance().getXComponentController(), // 设置XComponentController
      });
    }
    if (this.canAddNode) {
      this.xComponent.getParent()?.removeChild(this.xComponent);
      this.node.appendChild(this.xComponent);
    }
    return this.node;
  }

  // 重新添加typeNode节点
  addNode() {
    if (this.node !== null && this.node !== undefined) {
      console.info(TAG, "addNode id:"+(this.node?.getUniqueId())+" "+this.xComponent?.getUniqueId());
      this.node.appendChild(this.xComponent);
    }
  }

  // 移除typeNode节点
  removeNode() {
    if (this.node !== null && this.node !== undefined) {
      console.info(TAG, "removeNode");
      this.node.removeChild(this.xComponent);
    }
  }

  getNode(): typeNode.XComponent | null {
    console.info(TAG, "getNode is null:"+ (this.xComponent === null || this.xComponent === undefined))
    return this.xComponent;
  }
}
// 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();
  }
}

 以上示例代码对应的示意图如下所示:

应用使用单界面Ability时通过typeNode实现画中画功能

  1. 创建画中画控制器,注册生命周期事件以及控制事件回调。

    • 创建自定义NodeController,实现makeNode方法,在该方法中创建typeNode。
    • 通过create(config: PiPConfiguration, contentNode: typeNode.XComponent)接口创建画中画控制器实例。
    • 通过画中画控制器实例的setAutoStartEnabled接口设置是否需要在应用返回桌面时自动启动画中画。
    • 通过画中画控制器实例的on('stateChange')接口注册生命周期事件回调。
    • 通过画中画控制器实例的on('controlEvent')接口注册控制事件回调。

  2. 启动画中画。

    创建画中画控制器实例后,通过startPiP接口启动画中画,在画中画ABOUT_TO_START生命周期将typeNode节点从布局移除。

  3. 更新媒体源尺寸信息。

    画中画媒体源更新后(如切换视频),通过画中画控制器实例的updateContentSize接口更新媒体源尺寸信息,以调整画中画窗口比例。

  4. 关闭画中画。

    当不再需要显示画中画时,可根据业务需要,通过画中画控制器实例的stopPiP接口关闭画中画,在画中画ABOUT_TO_STOP生命周期将typeNode节点重新添加到布局中。

// EntryAbility.ets
import { UIAbility } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';

export default class EntryAbility extends UIAbility {
  onWindowStageCreate(windowStage: window.WindowStage): void {
    windowStage.loadContent('pages/Index', (err) => {
      // ...
    });
  }
}

 

// pages/Index.ets
import { PipManager } from '../model/PipManager';
import { PiPWindow } from '@kit.ArkUI'; // 引入PiPWindow模块

const TAG = 'Index'
@Entry
@Component
struct Index {
  private callback: Function = (state: PiPWindow.PiPState) => {
    if (state === PiPWindow.PiPState.ABOUT_TO_STOP) {
      // 画中画关闭或还原时触发ABOUT_TO_STOP生命周期,此时需要重新添加节点
      PipManager.getInstance().addNode();
    }
  };

  build() {
    Column() {
      Text('This is MainPage')
        .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%')
  }

  aboutToAppear(): void {
    PipManager.getInstance().registerLifecycleCallback(this.callback);
  }

  aboutToDisappear(): void {
    PipManager.getInstance().unregisterPipStateChangeListener();
    PipManager.getInstance().unRegisterLifecycleCallback(this.callback);
  }

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

  onPageHide(): void {
    console.info(TAG, 'onPageHide')
    PipManager.getInstance().setAutoStart(false);
  }
}
// model/XCNodeController.ets
import { FrameNode, NodeController, typeNode } from '@kit.ArkUI';
import { PipManager } from './PipManager';

const TAG = 'XCNodeController';

// 创建自定义NodeController
export class XCNodeController extends NodeController {
  xComponent: typeNode.XComponent | null = null;
  private node: FrameNode | null = null;
  private canAddNode: boolean = true;

  // 设置是否可以添加节点
  setCanAddNode(canAddNode: boolean) {
    this.canAddNode = canAddNode;
  }

  // 实现makeNode方法,当自定义NodeController被添加到布局时,该方法会被调用
  makeNode(context: UIContext): FrameNode | null {
    this.node = new FrameNode(context);
    this.node.commonAttribute
    if (this.xComponent === null || this.xComponent === undefined) {
      // 创建typeNode
      // let xc_options: XComponentOptions = {
      //   type: XComponentType.TEXTURE, // 类型设置为TEXTURE
      //   controller: PipManager.getInstance().getXComponentController(), // 设置XComponentController
      // }
      // this.xComponent = typeNode.createNode(context, "XComponent", xc_options);

      // 创建XComponent类型的typeNode
      this.xComponent = typeNode.createNode(context, "XComponent", {
        type: XComponentType.SURFACE, // 类型设置为SURFACE
        controller: PipManager.getInstance().getXComponentController(), // 设置XComponentController
      });
    }
    if (this.canAddNode) {
      this.xComponent.getParent()?.removeChild(this.xComponent);
      this.node.appendChild(this.xComponent);
    }
    return this.node;
  }

  // 重新添加typeNode节点
  addNode() {
    if (this.node !== null && this.node !== undefined) {
      console.info(TAG, "addNode");
      this.node.appendChild(this.xComponent);
    }
  }

  // 移除typeNode节点
  removeNode() {
    if (this.node !== null && this.node !== undefined) {
      console.info(TAG, "removeNode");
      this.node.removeChild(this.xComponent);
    }
  }

  getNode(): typeNode.XComponent | null {
    console.info(TAG, "getNode is null: "+ (this.xComponent === null || this.xComponent === undefined));
    return this.xComponent;
  }
}

 

// model/PipManager.ets
import { PiPWindow, typeNode } from '@kit.ArkUI'; // 引入PiPWindow模块
import { BusinessError } from '@kit.BasicServicesKit';
import { XCNodeController } from './XCNodeController';
import { AVPlayer} from '../model/AVPlayer'

// 自定义XComponentController
export class CustomXComponentController extends XComponentController {
  onSurfaceCreated(surfaceId: string): void {
    console.log(TAG, `onSurfaceCreated surfaceId: ${surfaceId}`);
    if (PipManager.getInstance().player.surfaceID === surfaceId) {
      return;
    }
    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);
  }

  // 将typeNode节点添加到原父节点
  addNode(): void {
    this.xcNodeController.addNode();
  }
}

以上示例代码对应的示意图如下所示:

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值