画中画效果实现
介绍
本示例通过@kit.ArkUI、@kit.MediaKit等接口,实现了视频播放、手动和自动拉起画中画、画中画窗口控制视频播放和暂停等功能。
效果预览
使用说明
- 在主界面,可以点击对应视频按钮进入视频播放页面;
- 视频播放页面点击开启,应用拉起画中画,点击关闭,关闭画中画;
- 视频播放页面点击自动开启画中画,在返回桌面时会自动拉起画中画;
- 视频播放页面会显示一些回调信息。
具体实现
- 整个示例用Navigation构建页面,主页面放置五个可点击视频框,点击之后进入视频播放页面。
- 进入视频播放页面后,有三块区域,最上方的XComponent,中间的画中画控制按钮以及下方的回调信息显示框。
- 点击开启后,应用手动拉起画中画,视频在画中画播放,返回桌面视频依旧画中画播放;点击关闭后,画中画播放的视频返回XComponent播放,同时返回桌面不会拉起画中画。
- 点击自动拉起画中画后,返回桌面时应用自动拉起画中画,视频画中画播放。
- 在播放页面进行画中画播放时,XComponent框会提示当前视频正在以画中画播放。
- 回调信息显示框会显示当前状态,错误原因以及按钮事件和状态,参考:[VideoPlay.ets]。
/*
* Copyright (c) 2024 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { PiPWindow } from '@kit.ArkUI';
import { JSON } from '@kit.ArkTS';
import { Constants } from '../constants/Constants';
import { AVPlayer } from './AVPlayer';
import Logger from '../utils/Logger';
const TAG = Constants.NAV_DESTINATION_NAME;
@Extend(Text)
function textType() {
.padding({ left: $r('app.integer.other_padding') })
.fontWeight(FontWeight.Bold)
.fontSize($r('app.integer.text_size'))
.alignSelf(ItemAlign.Start)
}
@Extend(Text)
function msgType() {
.padding({ left: $r('app.integer.other_padding') })
.fontSize($r('app.integer.text_size'))
.fontColor($r('app.color.Message_color'))
.alignSelf(ItemAlign.Start)
}
@Component
export struct PlayVideo {
@Consume('pageInfos') pageInfos: NavPathStack;
@State curState: string = '';
@State curError: string = '';
@State buttonAction: string = '';
@State isAutoPull: boolean = false;
@State isLightBackground: boolean = false;
@State hintMsgVisibility: boolean = false;
@State pipTypeString: string = '';
mXComponentController = new XComponentController();
surfaceId = '';
navigationId: string = '';
player?: AVPlayer;
pipController?: PiPWindow.PiPController;
eventHub = getContext().eventHub;
private scrollerForScroll: Scroller = new Scroller()
aboutToAppear(): void {
this.eventHub.on('onStateChange', (fg: boolean) => {
if (fg && this.curState === 'STARTED') {
this.stopPip();
}
});
}
async startPip() {
if (!this.pipController) {
await this.createPipController();
}
if (!this.pipController) {
Logger.info(`[${TAG}] pipController create error`);
return;
}
await this.pipController.startPiP();
}
async stopPip() {
if (!this.pipController) {
Logger.info(`[${TAG}] pipController is not exist`);
return;
}
await this.pipController.stopPiP();
}
async createPipController() {
this.pipController = await PiPWindow.create({
context: getContext(this),
componentController: this.mXComponentController,
navigationId: this.navigationId,
templateType: PiPWindow.PiPTemplateType.VIDEO_PLAY
});
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);
});
}
destroyPipController() {
if (!this.pipController) {
return;
}
this.pipController.off('stateChange');
this.pipController.off('controlPanelActionEvent');
this.pipController = undefined;
}
onStateChange(state: PiPWindow.PiPState, reason: string) {
switch (state) {
case PiPWindow.PiPState.ABOUT_TO_START:
this.curState = 'ABOUT_TO_START';
this.curError = Constants.ERROR_BY_DEFAULT;
break;
case PiPWindow.PiPState.STARTED:
this.curState = 'STARTED';
this.curError = Constants.ERROR_BY_DEFAULT;
break;
case PiPWindow.PiPState.ABOUT_TO_STOP:
this.curState = 'ABOUT_TO_STOP';
this.curError = Constants.ERROR_BY_DEFAULT;
break;
case PiPWindow.PiPState.STOPPED:
this.player?.updatePlayStatus(true);
this.player?.play();
this.curState = 'STOPPED';
this.curError = Constants.ERROR_BY_DEFAULT;
break;
case PiPWindow.PiPState.ABOUT_TO_RESTORE:
this.curState = 'ABOUT_TO_RESTORE';
this.curError = Constants.ERROR_BY_DEFAULT;
break;
case PiPWindow.PiPState.ERROR:
this.curState = 'ERROR';
this.curError = reason;
break;
default:
break;
}
Logger.info(`[${TAG}] onStateChange: ${this.curState}, reason: ${reason}`);
}
onActionEvent(event: PiPWindow.PiPActionEventType, status: number | undefined) {
switch (event) {
case 'playbackStateChanged':
if (status === 0) {
this.player?.updatePlayStatus(false);
this.player?.pause();
} else {
this.player?.updatePlayStatus(true);
this.player?.play();
}
break;
default:
break;
}
this.buttonAction = event + `-status:${status}`;
Logger.info(`[${TAG}] onActionEvent: ${this.buttonAction} status:${status}}`);
}
build() {
Stack() {
NavDestination() {
Column({ space: Constants.SPACE }) {
Stack() {
Text($r('app.string.current_video_pip_play'))
.fontColor($r('app.color.XComponent_text_color'))
.margin({ bottom: $r('app.integer.x_component_marg_bottom') })
.visibility(this.hintMsgVisibility ? Visibility.Visible : Visibility.Hidden)
XComponent({ id: 'video', type: 'surface', controller: this.mXComponentController })
.onLoad(() => {
Logger.info(`[${TAG}] XComponent onLoad`);
this.surfaceId = this.mXComponentController.getXComponentSurfaceId();
this.player = new AVPlayer(this.surfaceId, Constants.AVPLAYER_TYPE);
this.player.avPlayerFdSrc();
})
.onDestroy(() => {
this.player?.stopAvPlayer();
Logger.info(`[${TAG}] XComponent onDestroy`);
})
.size({ width: Constants.X_COMPONENT_WIDTH, height: $r('app.float.x_component_height') })
.margin({ top: $r('app.integer.x_component_marg_top') })
.backgroundColor(Color.Transparent)
.align(Alignment.Bottom)
.id('x_component')
}
.size({ width: Constants.X_COMPONENT_WIDTH, height: $r('app.float.x_component_height') })
.alignContent(Alignment.Bottom)
.backgroundColor($r('app.color.XComponent_backgroundColor'))
Scroll(this.scrollerForScroll) {
Column({ space: Constants.SPACE }) {
this.ControlPip()
this.AutoPip()
this.CallbackMessage()
}
.width(Constants.NAV_DESTINATION_WIDTH)
}
.layoutWeight(Constants.SCROLL_LAY_OUT_WEIGHT)
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.Off)
.edgeEffect(EdgeEffect.Spring)
}
.width(Constants.NAV_DESTINATION_WIDTH)
.height(Constants.NAV_DESTINATION_HEIGHT)
}
.hideTitleBar(true)
.backgroundColor($r('app.color.Play_backgroundColor'))
.onBackPressed(() => {
// Eject the top-of-the-stack element of the routing stack.
const popDestinationInfo = this.pageInfos.pop();
Logger.info('pop' + 'return value' + JSON.stringify(popDestinationInfo));
return true;
})
}
}
@Builder
ControlPip() {
Row({ space: Constants.SPACE }) {
Button($r('app.string.start'))
.width($r('app.integer.control_button_width'))
.onClick(() => {
this.startPip();
this.hintMsgVisibility = true;
})
Button($r('app.string.stop'))
.width($r('app.integer.control_button_width'))
.onClick(() => {
this.stopPip();
this.hintMsgVisibility = false;
})
}
.size({ width: Constants.CONTROL_WIDTH, height: $r('app.integer.control_height') })
.justifyContent(FlexAlign.SpaceAround)
.id('pip_control')
}
@Builder
AutoPip() {
Row() {
Text($r('app.string.auto'))
.width($r('app.integer.auto_text_width'))
.fontSize($r('app.integer.text_size'))
.fontWeight(FontWeight.Bold)
.padding({ left: $r('app.integer.other_padding') })
Toggle({ type: ToggleType.Switch, isOn: this.isAutoPull })
.width($r('app.integer.auto_button_width'))
.height($r('app.integer.auto_button_height'))
.selectedColor($r('app.color.Toggle_selectedColor'))
.padding({ right: $r('app.float.toggle_padding') })
.onChange(async (isOn: boolean) => {
this.isAutoPull = isOn;
if (!this.pipController) {
await this.createPipController();
}
this.pipController?.setAutoStartEnabled(this.isAutoPull);
this.hintMsgVisibility = true;
})
}
.width(Constants.AUTO_PIP_WIDTH)
.height($r('app.integer.auto_pip_height'))
.borderRadius($r('app.integer.auto_button_board_radius'))
.justifyContent(FlexAlign.SpaceBetween)
.backgroundColor($r('app.color.start_window_background'))
}
@Builder
CallbackMessage() {
Column({ space: Constants.SPACE }) {
Text($r('app.string.callback_message'))
.fontColor($r('app.color.Text_color'))
.padding({ right: $r('app.integer.callback_text_padding') })
Column() {
Text($r('app.string.current_status'))
.textType()
Text(this.curState)
.msgType()
}
.size({
width: Constants.CONTROL_WIDTH,
height: $r('app.integer.control_height')
})
.backgroundColor($r('app.color.Callback_message_backgroundColor'))
.borderRadius($r('app.integer.auto_button_board_radius'))
.justifyContent(FlexAlign.SpaceAround)
.id('current_state')
Column() {
Text($r('app.string.current_error'))
.textType()
Text(this.curError)
.msgType()
}
.size({
width: Constants.CONTROL_WIDTH,
height: $r('app.integer.control_height')
})
.backgroundColor($r('app.color.Callback_message_backgroundColor'))
.borderRadius($r('app.integer.auto_button_board_radius'))
.justifyContent(FlexAlign.SpaceAround)
.id('current_error')
Column() {
Text($r('app.string.current_action'))
.textType()
Text(this.buttonAction)
.msgType()
}
.size({
width: Constants.CONTROL_WIDTH,
height: $r('app.integer.control_height')
})
.backgroundColor($r('app.color.Callback_message_backgroundColor'))
.borderRadius($r('app.integer.auto_button_board_radius'))
.justifyContent(FlexAlign.SpaceAround)
.id('current_action')
}
}
}
以上就是本篇文章所带来的鸿蒙开发中一小部分技术讲解;想要学习完整的鸿蒙全栈技术。可以在结尾找我可全部拿到!
下面是鸿蒙的完整学习路线,展示如下:
除此之外,根据这个学习鸿蒙全栈学习路线,也附带一整套完整的学习【文档+视频】,内容包含如下:
内容包含了:(ArkTS、ArkUI、Stage模型、多端部署、分布式应用开发、音频、视频、WebGL、OpenHarmony多媒体技术、Napi组件、OpenHarmony内核、鸿蒙南向开发、鸿蒙项目实战)等技术知识点。帮助大家在学习鸿蒙路上快速成长!
鸿蒙【北向应用开发+南向系统层开发】文档
鸿蒙【基础+实战项目】视频
鸿蒙面经
为了避免大家在学习过程中产生更多的时间成本,对比我把以上内容全部放在了↓↓↓想要的可以自拿喔!谢谢大家观看!