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

article/2025/7/19 14:53:52

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

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。

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 = avPlayeravPlayer.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 = trueavPlayer.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) 指定视频播放的进度位置,并指定跳转模式。

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()口启动画中画。

// 该页面用于展示画中画功能的基本使用
//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: NavPathStackprivate 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 = avPlayeravPlayer.on('stateChange',(state)=>{if (state == 'initialized') {avPlayer.surfaceId = idavPlayer.prepare()}if (state == 'prepared') {avPlayer.loop = trueavPlayer.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的surfaceIdthis.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*/); // 默认为falsethis.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(管理器)

    // 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();}
}

配置页面

    // 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();}
}


http://www.hkcw.cn/article/ONRFQpMuWe.shtml

相关文章

res-downloader-视频号下载,网络视频资源嗅探下载器

适用系统&#xff1a;Windows(含Win7)、macOS 和 Linux 系统 一、核心功能与特性 全平台资源支持 支持微信视频号、抖音、快手、小红书等短视频平台的无水印视频下载&#xff0c;同时兼容酷狗音乐、QQ音乐、微信小程序等音频和多媒体资源 1 8。覆盖视频、音频、图片、m3u8流媒体…

通义万相2.1:开启视频生成新时代

文章摘要&#xff1a;通义万相 2.1 是一款在人工智能视频生成领域具有里程碑意义的工具&#xff0c;它通过核心技术的升级和创新&#xff0c;为创作者提供了更强大、更智能的创作能力。本文详细介绍了通义万相 2.1 的背景、核心技术、功能特性、性能评测、用户反馈以及应用场景…

计算机视觉——基于树莓派的YOLO11模型优化与实时目标检测、跟踪及计数的实践

概述 设想一下&#xff0c;你在多地拥有多个仓库&#xff0c;要同时监控每个仓库的实时状况&#xff0c;这对于时间和精力而言&#xff0c;都构成了一项艰巨挑战。从成本和可靠性的层面考量&#xff0c;大规模部署计算设备也并非可行之策。一方面&#xff0c;大量计算设备的购…

AI赋能视频创作:蓝耘MaaS与海螺AI技术的深度融合

云边有个稻草人-CSDN博客 目录 一、蓝耘MaaS平台概述 &#xff08;1&#xff09;平台的模块化设计 &#xff08;2&#xff09;蓝耘MaaS的灵活性与扩展性 &#xff08;3&#xff09;蓝耘MaaS的安全性与隐私保护 二、海螺AI视频模型简介 &#xff08;1&#xff09;海螺AI的…

6条视频涨粉千万 心中之城回应质疑 新IP崛起之路

DY平台再次见证了一个涨粉神话。一部剧,六条视频,让账号「心中之城」在短短时间内涨粉1000万。从四月发布第一条视频至今,该账号已跃居DY热榜榜首。各大媒体纷纷报道这一现象。「心中之城」通过解说英剧《豺狼的日子》吸引了大量关注。该账号的运营者曾是电影圈知名账号毒舌…

外卖员不用办健康证了?网友争论 食品安全引热议

点外卖已成为很多人的生活习惯,而网络订餐配送过程中的食品安全问题也备受关注。去年底,四川省卫生健康委与市场监管局联合发布新规,明确外卖送餐人员及预包装食品销售从业者无须办理传统健康证即可上岗,并要求体检机构停止为外卖员提供健康证服务。这一消息受到不少外卖小…

ubuntu系统更换镜像源

目录 前言 一 查看操作系统版本 二 备份镜像源 三 镜像源站点官网 四 修改配置文件 前言 ubuntu系统默认官方源&#xff08;archive.ubuntu.com&#xff09;通常位于国外&#xff0c;国内用户访问时网络延迟高、带宽受限&#xff0c;导致下载软件速度很慢或者直接遇到更新失…

南京“以债换房”可置换月供?假 网传信息为谣言

近日,一些账号如“南京二手房零首付李经理”、“合肥瑶珺房地产代理有限公司”、“中墅地产唐广君”、“清、静”、“杨哥”、“南京免首付房产管家”等发布帖子称,南京开放了“以债换房”政策,可以将网络债务直接置换为房子的月供,无需支付首付。经南京市房产部门和人行江…

QT入门学习(一)---新建工程与、信号与槽

一: 新建QT项目 二:QT文件构成 2.1 first.pro 项目管理文件&#xff0c;下面来看代码解析 QT core guigreaterThan(QT_MAJOR_VERSION, 4): QT widgetsCONFIG c11TARGET main# The following define makes your compiler emit warnings if you use # any Qt feature …

自己烧水喝是否比买桶装水更健康 减少微塑料摄入

水是构成人体的重要物质,对维持正常的身体活动和认知能力至关重要。尤其是在天气炎热时,及时补充水分尤为重要。在日常生活中,有人习惯自己烧水喝,也有人因担心自来水水质问题而选择桶装水或瓶装水。那么,这两种饮水方式哪种更健康呢?一项发表在《美国国家科学院院刊》上…

PCB设计教程【强化篇】——USB拓展坞PCB布线

前言 本教程基于B站Expert电子实验室的PCB设计教学的整理&#xff0c;为个人学习记录&#xff0c;旨在帮助PCB设计新手入门。所有内容仅作学习交流使用&#xff0c;无任何商业目的。若涉及侵权&#xff0c;请随时联系&#xff0c;将会立即处理 目录 前言 一、前期准备与规则…

1.RV1126-OPENCV 交叉编译

一.下载opencv-3.4.16.zip到自己想装的目录下 二.解压并且打开 opencv 目录 先用 unzip opencv-3.4.16.zip 来解压 opencv 的压缩包&#xff0c;并且进入 opencv 目录(cd opencv-3.4.16) 三. 修改 opencv 的 cmake 脚本的内容 先 cd platforms/linux 然后修改 arm-gnueabi.to…

黑龙江多地出现罕见粉色极光 梦幻粉紫点亮夜空

6月2日,多位网友在黑龙江省密山市、佳木斯市等地拍摄到了罕见的粉色极光。整个天空被渲染成梦幻般的粉紫色,景象如梦如幻,宛如仙境。一位视频发布者表示,他在6月1日晚上9时左右开始在佳木斯市郊区福胜村江边拍摄,一直持续到次日凌晨1时,使用的是延时摄影技术。他表示,佳…

官方通报!常永春已被“双开” 多次违规接受宴请

中央纪委国家监委官网通报,北京市纪委监委公开了四起违规吃喝典型问题。其中一起涉及北京城建集团有限责任公司原党委书记、董事长常永春。2021年至2024年间,常永春多次在私人会所接受私营企业主安排的宴请,并饮用高档酒水。他还存在其他严重违纪违法问题,最终被开除党籍和…

JS入门——Array

JS入门——Array 一、概述 二、案例代码&#xff08;请自行进行解注释和注释来实现调试&#xff09; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>数组的定义</title> </head> <b…

MySQL读写分离

GTID一主多从搭建异步复制 半同步复制| 机器 | ip | | ------ | -------------- | | master | 192.168.66.143 | | slave1 | 192.168.66.144 | | slave2 | 192.168.66.145 | > 将144的数据复制到145去&#xff0c;删除数据目录下的auto.cnf过程省略读写分离读…

老人花105万元买基金 两年多亏30万元 状告银行求赔偿

一位年过八旬的投资者在2021年投入105万元购买了一只公募基金产品,两年多时间亏损约30万元。该投资者随后诉至法院,要求相关代销银行承担赔偿责任。该案先后经过两次审理。一审法院结合双方的过错程度,判决银行承担70%损失赔偿责任并支付损失利息。二审法院则认为,投资者自…

慈星股份可交债往事撕开资本隐秘角落 内幕操作浮出水面

慈星股份(300307.SZ)在5个月内连推两起重大资产重组,引发市场广泛关注。随着这些动作,公司过去资本运作的内幕逐渐浮出水面。知情人士陈曦举报称,慈星股份控股股东宁波裕人智慧科技(集团)有限公司在2017年发行的可交换债项目中存在违规行为。据称,宁波裕人通过两家第三…

解锁帕金森人群的活力锻炼法

帕金森人群通过科学锻炼&#xff0c;能有效维持身体机能。首先推荐 “原地踏步操”&#xff0c;在家中安全区域踮脚抬高膝盖&#xff0c;配合手臂前后摆动&#xff0c;每次坚持 5 分钟&#xff0c;可增强下肢力量&#xff0c;改善肢体协调性。也可利用墙面进行 “推墙练习”&am…

鏖战近3小时!22岁郑钦文躺地庆祝 首进法网8强创历史

在北京时间6月1日晚结束的法网女单1/8决赛中,中国选手郑钦文经过近3小时的激战,以2-1战胜赛会19号种子萨姆索诺娃,首次跻身法网女单8强。这场胜利让她成为公开赛年代第二位闯入法网单打八强的中国选手。比赛结束后,22岁的郑钦文激动地躺在地上庆祝。作为赛会8号种子,郑钦文…