前言
本示例主要介紹視頻小窗口播放場景,利用媒體的AVPlayer實現視頻播放以及相關操作,利用PiPWindow開啓懸浮窗從而實現小窗口播放視頻。
效果圖預覽
使用説明
- 等待視頻加載完成,視頻會自動播放。
- 將應用隱藏到後台,自動拉起懸浮窗繼續播放視頻。
- 點擊懸浮窗恢復圖標,恢復到原始播放界面,視頻繼續正常播放。
- 原始播放界面視頻暫停不會拉起懸浮窗。
- 懸浮窗視頻暫停後,再點擊恢復圖標,原始播放界面視頻繼續播放。
- 懸浮窗點擊關閉之後,原始播放界面視頻暫停。
- 點擊原視頻界面小窗口圖標,可開啓懸浮窗。
- 手指在原視頻左側滑動可改變視頻頁面的亮度(需真機驗證)。
- 手指在原視頻右側滑動可改變視頻的聲音(需真機驗證,注:本案例使用的視頻暫無聲音,開發者可更換視頻資源驗證該功能)。
下載安裝
模塊oh-package.json5文件中引入依賴
"dependencies": {
"@ohos/compressfile": "har包地址"
}
ets文件import自定義視圖實視頻懸浮窗組件
import { PipWindowComponent } from '@ohos/pipwindow';
快速使用
本節主要介紹瞭如何快速上手使用視頻懸浮窗組件,包括調節視頻亮度聲音控制器組件以及常見自定義參數的初始化。
- 構建組件
在代碼合適的位置使用PipWindowComponent組件並傳入對應的參數,後續將介紹對應參數的初始化。
/**
* 畫中畫控制開啓、播放組件
* player:初始化視頻播放控制器
* url:傳入在線視頻資源
*/
PipWindowComponent({
player: this.player,
url: this.url
})
各參數初始化,player可直接寫PipManager.getInstance().player,url必須為在線mp4視頻。
@State player: AVPlayer = PipManager.getInstance().player; // 初始化視頻播放控制器
@State url: string = " "; // 傳入在線視頻資源
屬性(接口)説明
PipWindowComponent組件屬性
|
屬性
|
類型
|
釋義
|
默認值
|
|
player
|
AVPlayer
|
初始化視頻播放控制器
|
-
|
|
url
|
string
|
傳入在線視頻資源
|
-
|
實現思路
本例涉及的關鍵特性和實現方案如下:
- 使用媒體的AVPlayer實現視頻播放。
/**
* 初始化AVPlayer
* @param url 在線視頻路徑
* @returns 返回值將在線視頻進行綁定
*/
async init(url: string): Promise<void> {
await this.release();
// 創建avPlayer實例對象
this.avPlayer = await media.createAVPlayer();
this.isCreate = true;
// 創建狀態機變化回調函數
await this.setSourceInfo(); // 視頻信息上報函數
await this.setStateChangeCallback(); // 狀態機上報回調函數
this.avPlayer.url = url; // 播放hls網絡直播碼流
}
- 使用PiPWindow開啓懸浮窗從而實現小窗口播放視頻。
/**
* 創建畫中畫控制器,註冊生命週期事件以及控制事件回調
* @param ctx 上下文環境
*/
init(ctx: Context) {
if (this.pipController !== null && this.pipController !== undefined) {
return;
}
// 當前設備如若不支持畫中畫則退出
if (!PiPWindow.isPiPEnabled()) {
return;
}
let config: PiPWindow.PiPConfiguration = {
context: ctx,
// XComponent組件綁定同一個
componentController: this.getXComponentController(),
// 畫中畫媒體類型枚舉,當前使用的視頻播放模版
templateType: PiPWindow.PiPTemplateType.VIDEO_PLAY,
};
// 通過create接口創建畫中畫控制器實例
let promise: Promise<PiPWindow.PiPController> = PiPWindow.create(config);
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(`Failed to create pip controller. Cause:${err.code}, message:${err.message}`);
});
}
- 通過綁定同一個XComponent控制器使得視頻頁面和懸浮窗頁面視頻保持統一播放進度。
XComponent({
type: XComponentType.SURFACE,
controller: PipManager.getInstance().getXComponentController()
})
.onLoad(() => {
// 將surfaceId設置給媒體源
PipManager.getInstance()
.getXComponentController()
.onSurfaceCreated(PipManager.getInstance().getXComponentController().getXComponentSurfaceId())
// 初始化AVPlayer
PipManager.getInstance().player.init(this.url);
})
- 使用@Watch監聽AVPlayer的發生變化時,會觸發onPlayingChange的回調方法。組件中需要手動控制視頻的播放與暫停,因為視頻的播放狀態是需要根據視頻加載進度和手動控制來改變的,所以可以使用@Watch進行監聽。
@ObjectLink @Watch("onPlayingChange") player: AVPlayer
onPlayingChange() {
this.player.isPlaying ? this.player.getPlay() : this.player.getPause();
if (this.player.isPlaying === false) {
PipManager.getInstance().setAutoStart(false)
} else {
PipManager.getInstance().setAutoStart(true)
PipManager.getInstance().updatePiPControlStatus()
}
}
- 懸浮窗的從小窗口恢復到原始播放界面以及關閉懸浮窗,需要通過相關生命週期來進行控制視頻的播放狀態。
// 監聽畫中畫生命週期
onStateChange(state: PiPWindow.PiPState, reason: string) {
switch (state) {
// 表示畫中畫將要啓動
case PiPWindow.PiPState.ABOUT_TO_START:
break;
// 表示畫中畫已經啓動
case PiPWindow.PiPState.STARTED:
// 啓動畫中畫
PipManager.getInstance().player.isPiPWindowLoad = true;
break;
// 表示畫中畫將要停止
case PiPWindow.PiPState.ABOUT_TO_STOP:
// 畫中畫關閉
PipManager.getInstance().player.isPiPWindowLoad = false;
break;
// 表示畫中畫已經停止
case PiPWindow.PiPState.STOPPED:
// 畫中畫關閉
PipManager.getInstance().player.isPiPWindowLoad = false;
// 小窗口點擊關閉畫中畫,視頻暫停播放
if (!PipManager.getInstance().player.isPiPWindowRestore) {
PipManager.getInstance().player.isPlaying = false;
}
PipManager.getInstance().player.isPiPWindowRestore = false;
break;
// 表示畫中畫將從小窗播放恢復到原始播放界面
case PiPWindow.PiPState.ABOUT_TO_RESTORE:
// 小窗口復原關閉畫中畫
PipManager.getInstance().player.isPiPWindowLoad = false;
// 從小窗口復原
PipManager.getInstance().player.isPiPWindowRestore = true;
if (!PipManager.getInstance().player.isPlaying) {
PipManager.getInstance().player.isPlaying = true;
}
break;
// 表示畫中畫生命週期執行過程出現了異常
case PiPWindow.PiPState.ERROR:
break;
default:
break;
}
}
- 因為當懸浮窗的從小窗口恢復到原始播放界面以及關閉懸浮窗時對視頻播放狀態進行操作,懸浮窗的播放圖標並不會實時更新,所以手動進行更新。
/**
* 視頻播放頁面
* @param url:視頻源(本案例僅支持使用在線視頻)
*/
XComponentView({
url: this.url
})
.gesture(
// 雙擊視頻,視頻暫停/播放
GestureGroup(GestureMode.Exclusive,
TapGesture({ count: TAP_GESTURE }).onAction((event?: GestureEvent) => {
PipManager.getInstance().player.isPlaying = !PipManager.getInstance().player.isPlaying
if (PipManager.getInstance().player.isPlaying === false) {
PipManager.getInstance().setAutoStart(false)
PipManager.getInstance().updatePiPControlStatus()
} else {
PipManager.getInstance().setAutoStart(true)
PipManager.getInstance().updatePiPControlStatus()
}
})
))
// 更新畫中畫面板控件狀態
updatePiPControlStatus() {
let controlType: PiPWindow.PiPControlType = PiPWindow.PiPControlType.VIDEO_PLAY_PAUSE; // 視頻播放控制面板中播放/暫停控件。
let status: PiPWindow.PiPControlStatus = PiPWindow.PiPControlStatus.PLAY; // 視頻播放控制面板中播放/暫停控件為播放狀態。
this.pipController?.updatePiPControlStatus(controlType, status); // 更新控制面板控件功能狀態
}
- 使用onAreaChange來獲取原視頻頁面的播放區域寬高。
Stack(this.player.isLoading ? { alignContent: Alignment.Center } :
{ alignContent: Alignment.Bottom }) {
/**
* 視頻播放組件
* url:視頻源(本案例僅支持使用在線視頻)
*/
XComponentView({
url: this.url
})
...
}
.onAreaChange((oldVal: Area, newVal: Area) => {
// 獲取視頻播放區域的寬高
this.videoAreaWidth = newVal.width as number;
this.videoAreaHeight = newVal.height as number;
})
- 使用PanGesture來確定手指滑動的方向以及移動的距離從而改變實時改變視頻的亮度和聲音。
Stack(this.player.isLoading ? { alignContent: Alignment.Center } :
{ alignContent: Alignment.Bottom }) {
/**
* 視頻播放組件
* url:視頻源(本案例僅支持使用在線視頻)
*/
XComponentView({
url: this.url
})
...
}
.gesture(
// 雙擊視頻,視頻暫停/播放
GestureGroup(GestureMode.Exclusive,
TapGesture({ count: TAP_GESTURE })
.onAction((event?: GestureEvent) => {
this.player.isPlaying = !this.player.isPlaying;
AppStorage.setOrCreate('pipWindow_isPlaying', this.player.isPlaying);
if (this.player.isPlaying === false) {
PipManager.getInstance().setAutoStart(false);
PipManager.getInstance().updatePiPControlStatus();
} else {
PipManager.getInstance().setAutoStart(true);
PipManager.getInstance().updatePiPControlStatus();
}
}),
PanGesture(this.panOption)
.onActionStart((event: GestureEvent) => {
this.sysVolumeChange();
this.positionH = event.offsetY;
this.controlShow = true;
})
.onActionUpdate((event: GestureEvent) => {
this.panOption.setDirection(PanDirection.Vertical);
// 手指初次滑動橫向座標位置
this.fingerPosition = event.fingerList[0].localX;
if (this.positionH === event.offsetY) {
return;
}
// 手指移動的距離
let changeVolume = ((this.positionH - event.offsetY) / this.videoAreaHeight) * HEIGHT_INTEGER;
if (this.fingerPosition < (this.videoAreaWidth / PARTITION)) {
this.onBrightActionUpdate(changeVolume);
// 調節視頻亮度
this.mWindow?.setWindowBrightness(this.bright / CONTROLLER_MAX);
} else {
this.onVolumeActionUpdate(changeVolume);
}
this.positionH = event.offsetY;
})
.onActionEnd(() => {
// 延時隱藏控制器
setTimeout(() => {
this.controlShow = false;
}, CONTROLLER_DElAY)
})
))