HarmonyOS6 - XComponent与AVPlayer实现视频播放功能
1. 自定义渲染 (XComponent)
1. 概述
XComponent组件作为一种渲染组件,可用于EGL/OpenGLES和媒体数据写入,通过使用XComponent持有的"NativeWindow"来渲染画面,通常用于满足开发者较为复杂的自定义渲染需求,例如相机预览流的显示和游戏画面的渲染。其可通过指定type字段来实现不同的渲染方式,分别为XComponentType.SURFACE和XComponentType.TEXTURE。对于SURFACE类型,开发者将定制的绘制内容单独展示到屏幕上。对于TEXTURE类型,开发者将定制的绘制内容和XComponent组件的内容合成后展示到屏幕上。
XComponent持有一个Surface,开发者能通过调用NativeWindow等接口,申请并提交Buffer至图形队列,以此方式将自绘制内容传送至该Surface。XComponent负责将此Surface整合进UI界面,其中展示的内容正是开发者传送的自绘制内容。Surface的默认位置与大小与XComponent组件一致,开发者可利用setXComponentSurfaceRect接口自定义调整Surface的位置和大小。XComponent组件负责创建Surface,并通过回调将Surface的相关信息告知应用。应用可以通过一系列接口设定Surface的属性。该组件本身不对所绘制的内容进行感知,亦不提供渲染绘制的接口。
目前XComponent组件主要有三个应用场景:
| XComponent组件应用场景 | 场景简介 | 场景特点 |
|---|---|---|
| 使用XComponentController管理Surface生命周期场景 | 该场景在ArkTS侧的XComponentController获取SurfaceId,生命周期回调、触摸、鼠标、按键等事件回调等均在ArkTS侧触发。 | 适用于视频播放、相机预览等媒体播放类场景,该场景需要在ArkTS侧获取SurfaceId,并将SurfaceId传入对应接口。 |
| 使用OH_ArkUI_SurfaceHolder管理Surface生命周期场景 | 该场景根据XComponent组件对应的ArkUI_NodeHandle创建OH_ArkUI_SurfaceHolder,生命周期回调、触摸等事件回调、无障碍和可变帧率回调等均在Native侧触发。 | 适用于如下场景:1.有较复杂的交互逻辑、对频繁跨语言调用导致性能损耗敏感的场景。2.希望能控制Surface生命周期触发时机的场景。 |
| 使用NativeXComponent管理Surface生命周期场景 | 该场景在native层获取Native XComponent实例,在Native侧注册XComponent的生命周期回调,以及触摸、鼠标、按键等事件回调。 | 与使用OH_ArkUI_SurfaceHolder管理Surface生命周期场景类似,但交互事件接口不够丰富,且使用不当容易出现稳定性问题,建议使用OH_ArkUI_SurfaceHolder的接口。 |
2. 约束与限制
当开发者传输的绘制内容包含透明元素时,Surface区域的显示效果会与下方内容进行合成展示。例如,若传输的内容完全透明,且XComponent的背景色被设置为黑色,同时Surface保持默认的大小与位置,则最终显示的将是一片黑色区域。
2. AVPlayer
1. 概述
当前提供两种视频播放开发的方案:
- AVPlayer:功能较完善的音视频播放ArkTS/JS API,集成了流媒体和本地资源解析,媒体资源解封装,视频解码和渲染功能,适用于对媒体资源进行端到端播放的场景,可直接播放mp4、mkv等格式的视频文件。
- Video组件:封装了视频播放的基础能力,需要设置数据源及基础信息即可播放视频,但相对扩展能力较弱。Video组件由ArkUI提供能力,相关指导请参考UI开发文档-Video组件。
本开发指导将介绍如何使用AVPlayer开发视频播放功能,以完整播放一个视频作为示例,实现端到端播放原始媒体资源。
播放的全流程包含:创建AVPlayer,设置播放资源和窗口,设置播放参数(音量/倍速/缩放模式),播放控制(播放/暂停/跳转/停止),重置,销毁资源。在进行应用开发的过程中,开发者可以通过AVPlayer的state属性主动获取当前状态或使用on('stateChange')方法监听状态变化。如果应用在视频播放器处于错误状态时执行操作,系统可能会抛出异常或生成其他未定义的行为。
播放状态变化示意图

状态的详细说明请参考AVPlayerState。当播放处于prepared / playing / paused / completed状态时,播放引擎处于工作状态,这需要占用系统较多的运行内存。当客户端暂时不使用播放器时,调用reset()或release()回收内存资源,做好资源利用。
2. 开发建议
当前指导仅介绍如何实现媒体资源播放,在应用开发过程中可能会涉及后台播放、播放冲突等情况,请根据实际需要参考以下说明。
- 如果要实现后台播放或熄屏播放,需要接入AVSession(媒体会话)和申请长时任务,避免播放被系统强制中断。
- 应用在播放过程中,若播放的媒体数据涉及音频,根据系统音频管理策略(参考处理音频焦点事件),可能会被其他应用打断,建议应用主动监听音频打断事件,根据其内容提示,做出相应的处理,避免出现应用状态与预期效果不一致的问题。
- 面对设备同时连接多个音频输出设备的情况,应用可以通过on('audioOutputDeviceChangeWithInfo')监听音频输出设备的变化,从而做出相应处理。
- 如果需要访问在线媒体资源,需要申请 ohos.permission.INTERNET 权限。
3. 开发步骤及注意事项
(1)调用createAVPlayer()创建AVPlayer实例,初始化进入idle状态。
js
import { media } from '@kit.MediaKit';
// 创建avPlayer实例对象。
let avPlayer = await media.createAVPlayer();
(2)设置业务需要的监听事件,搭配全流程场景使用。支持的监听事件包括:
| 事件类型 | 说明 |
|---|---|
| stateChange | 必要事件,监听播放器的state属性改变。需要播放器在idle状态下、未调用设置资源接口前完成设置监听,若在调用设置资源接口后再设置监听,可能导致无法收到资源设置过程中上报的stateChange事件。 |
| error | 必要事件,监听播放器的错误信息。需要播放器在idle状态下、未调用设置资源接口前完成设置监听,若在调用设置资源接口后再设置监听,可能导致无法收到资源设置过程中上报的error事件。 |
| durationUpdate | 用于进度条,监听进度条长度,刷新资源时长。 |
| timeUpdate | 用于进度条,监听进度条当前位置,刷新当前时间。 |
| seekDone | 响应API调用,监听seek()请求完成情况。当使用seek()跳转到指定播放位置后,如果seek操作成功,将上报该事件。 |
| speedDone | 响应API调用,监听setSpeed()请求完成情况。当使用setSpeed()设置播放倍速后,如果setSpeed操作成功,将上报该事件。 |
| volumeChange | 响应API调用,监听setVolume()请求完成情况。当使用setVolume()调节播放音量后,如果setVolume操作成功,将上报该事件。 |
| bitrateDone | 响应API调用,用于HLS协议流,监听setBitrate()请求完成情况。当使用setBitrate()指定播放比特率后,如果setBitrate操作成功,将上报该事件。 |
| availableBitrates | 用于HLS协议流,监听HLS资源的可选bitrates,用于setBitrate()。 |
| bufferingUpdate | 用于网络播放,监听网络播放缓冲信息。 |
| startRenderFrame | 用于视频播放,监听视频播放首帧渲染时间。当AVPlayer首次起播进入playing状态后,等到首帧视频画面被渲染到显示画面时,将上报该事件。应用通常可以利用此事件上报,进行视频封面移除,达成封面与视频画面的顺利衔接。 |
| videoSizeChange | 用于视频播放,监听视频播放的宽高信息,可用于调整窗口大小、比例。 |
| audioInterrupt | 监听音频焦点切换信息,搭配属性audioInterruptMode使用。如果当前设备存在多个媒体正在播放,音频焦点被切换(即播放其他媒体如通话等)时将上报该事件,应用可以及时处理。 |
js
// 此处仅为示例,开发者根据需要设置合适的监听事件。
import { BusinessError } from '@kit.BasicServicesKit';
import { audio } from '@kit.AudioKit';
avPlayer.on('stateChange', async (state: string, reason: media.StateChangeReason) => {
// 开发者根据需要写入业务逻辑。
});
avPlayer.on('error', (error: BusinessError) => {
// 开发者根据需要写入业务逻辑。
});
avPlayer.on('durationUpdate', (duration: number) => {
// 开发者根据需要写入业务逻辑。
});
avPlayer.on('timeUpdate', (time:number) => {
// 开发者根据需要写入业务逻辑。
});
avPlayer.on('seekDone', (seekDoneTime:number) => {
// 开发者根据需要写入业务逻辑。
});
avPlayer.on('speedDone', (speed:number) => {
// 开发者根据需要写入业务逻辑。
});
avPlayer.on('volumeChange', (vol: number) => {
// 开发者根据需要写入业务逻辑。
});
avPlayer.on('bitrateDone', (bitrate:number) => {
// 开发者根据需要写入业务逻辑。
});
avPlayer.on('availableBitrates', (bitrates: Array<number>) => {
// 开发者根据需要写入业务逻辑。
});
avPlayer.on('bufferingUpdate', (infoType: media.BufferingInfoType, value: number) => {
// 开发者根据需要写入业务逻辑。
});
avPlayer.on('startRenderFrame', () => {
// 开发者根据需要写入业务逻辑。
});
avPlayer.on('videoSizeChange', (width: number, height: number) => {
// 开发者根据需要写入业务逻辑。
});
avPlayer.on('audioInterrupt', (info: audio.InterruptEvent) => {
// 开发者根据需要写入业务逻辑。
});
(3)设置资源:设置属性url,AVPlayer进入initialized状态。
说明:
下面代码示例中的url仅作示意使用,开发者需根据实际情况,确认资源有效性并设置:
- 如果使用本地资源播放,必须确认资源文件可用,并使用应用沙箱路径访问对应资源,参考获取应用文件路径。应用沙箱的介绍及如何向应用沙箱推送文件,请参考文件管理。
- 如果使用网络播放路径,需声明权限:ohos.permission.INTERNET。
- 如果使用ResourceManager.getRawFd打开HAP资源文件描述符,使用方法可参考ResourceManager API参考。
- 需要使用支持的播放格式与协议。
js
let url = 'https://xxx.xxx.xxx.mp4';
if (avPlayer == null) {
return;
}
avPlayer.url = url;
(4)设置窗口:获取并设置属性SurfaceID,用于设置显示画面。
应用需要从XComponent组件获取surfaceID
js
// 通过接口getXComponentSurfaceId获取surfaceId。
let surfaceId = '';
if (avPlayer == null) {
return;
}
if (surfaceId === '') {
return;
}
avPlayer.surfaceId = surfaceId;
(5)准备播放:调用prepare(),AVPlayer进入prepared状态,此时可以获取duration,设置缩放模式、音量等。
js
import { BusinessError } from '@kit.BasicServicesKit';
avPlayer.prepare((err: BusinessError) => {
if (err) {
console.error('Failed to prepare,error message is :' + err.message);
} else {
console.info('Succeeded in preparing');
}
});
(6)视频播控:播放play(),暂停pause(),跳转seek(),停止stop() 等操作。
js
import { BusinessError } from '@kit.BasicServicesKit';
// 播放操作。
avPlayer.play().then(() => {
console.info('Succeeded in playing');
}, (err: BusinessError) => {
console.error('Failed to play,error message is :' + err.message);
});
// 暂停操作。
avPlayer.pause((err: BusinessError) => {
if (err) {
console.error('Failed to pause,error message is :' + err.message);
} else {
console.info('Succeeded in pausing');
}
});
// 跳转操作。
let seekTime: number = 1000;
avPlayer.seek(seekTime, media.SeekMode.SEEK_PREV_SYNC);
// 停止操作。
avPlayer.stop((err: BusinessError) => {
if (err) {
console.error('Failed to stop,error message is :' + err.message);
} else {
console.info('Succeeded in stopping');
}
});
(7)(可选)更换资源:调用reset()重置资源,AVPlayer重新进入idle状态,允许更换资源url。
js
import { BusinessError } from '@kit.BasicServicesKit';
avPlayer.reset((err: BusinessError) => {
avPlayer.url = url;
if (err) {
console.error('Failed to reset,error message is :' + err.message);
} else {
console.info('Succeeded in resetting');
}
});
// 更换url。
let url = 'https://xxx.xxx.xxx.mp4';
if (avPlayer == null) {
return;
}
avPlayer.url = url;
(8)退出播放:调用release()销毁实例,AVPlayer进入released状态,退出播放。
js
import { BusinessError } from '@kit.BasicServicesKit';
avPlayer.release((err: BusinessError) => {
if (err) {
console.error('Failed to release,error message is :' + err.message);
} else {
console.info('Succeeded in releasing');
}
});
更详细的API参考文档地址:https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-media-avplayer
3. 实战案例
基于以上知识点,我们使用视频播放开发方案一,使用AVPlayer结合XComponent做一个实战页面
1. 效果
AVPlayer实现视频播放功能
2. 代码
js
import display from '@ohos.display';
import emitter from '@ohos.events.emitter';
import { common } from '@kit.AbilityKit';
import media from '@ohos.multimedia.media';
const PROPORTION = 0.99; // 占屏幕比例
const SURFACE_W = 0.9; // 表面宽比例
const SURFACE_H = 1.78; // 表面高比例
const SET_INTERVAL = 100; // interval间隔时间
const TIME_ONE = 60000;
const TIME_TWO = 1000;
const SPEED_ZERO = 0;
const SPEED_ONE = 1;
const SPEED_TWO = 2;
const SPEED_THREE = 3;
const SPEED_COUNT = 4;
let innerEventFalse: emitter.InnerEvent = {
eventId: 1,
priority: emitter.EventPriority.HIGH
};
let innerEventTrue: emitter.InnerEvent = {
eventId: 2,
priority: emitter.EventPriority.HIGH
};
let innerEventWH: emitter.InnerEvent = {
eventId: 3,
priority: emitter.EventPriority.HIGH
};
/**
* 视频播放案例
*/
@Entry
@Component
struct Page05 {
tag: string = 'AVPlayManager';
private xComponentController: XComponentController = new XComponentController();
private avPlayer: media.AVPlayer | null = null;
private surfaceId: string = '';
private intervalID: number = -1;
private seekTime: number = -1;
private context: common.UIAbilityContext | undefined = undefined;
@State title: Resource = $r('app.string.EntryAbility_label');
@State fileName: string = 'test.mp4'; //rawfile目录下的视频文件
@State isSwiping: boolean = false; // 用户滑动过程中
@State isPaused: boolean = true; // 暂停播放
@State XComponentFlag: boolean = false;
@State speedSelect: number = 0; // 倍速选择
@State speedList: Resource[] =
[$r('app.string.video_speed_1_0X'), $r('app.string.video_speed_1_25X'), $r('app.string.video_speed_1_75X'),
$r('app.string.video_speed_2_0X')];
@StorageLink('durationTime') durationTime: number = 0; // 视频总时长
@StorageLink('currentTime') currentTime: number = 0; // 视频当前时间
@StorageLink('speedName') speedName: Resource = $r('app.string.video_speed_1_0X');
@StorageLink('speedIndex') speedIndex: number = 0; // 倍速索引
@State surfaceW: number | null = null;
@State surfaceH: number | null = null;
@State percent: number = 0;
@State windowWidth: number = 300;
@State windowHeight: number = 200;
getDurationTime(): number {
return this.durationTime;
}
getCurrentTime(): number {
return this.currentTime;
}
timeConvert(time: number): string {
let min: number = Math.floor(time / TIME_ONE);
let second: string = ((time % TIME_ONE) / TIME_TWO).toFixed(0);
// return `${min}:${(+second < TIME_THREE ? '0' : '') + second}`;
second = second.padStart(2, '0');
return `${min}:${second}`;
}
async msleepAsync(ms: number): Promise<boolean> {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(true)
}, ms)
})
}
async avSetupVideo() {
// 通过UIAbilityContext的resourceManager成员的getRawFd接口获取媒体资源播放地址。
// 返回类型为{fd,offset,length},fd为HAP包fd地址,offset为媒体资源偏移量,length为播放长度。
if (this.context == undefined) {
return;
}
let fileDescriptor = await this.context.resourceManager.getRawFd(this.fileName);
let avFileDescriptor: media.AVFileDescriptor =
{ fd: fileDescriptor.fd, offset: fileDescriptor.offset, length: fileDescriptor.length };
if (this.avPlayer) {
console.info(`${this.tag}: init avPlayer release2createNew`);
this.avPlayer.release();
await this.msleepAsync(1500);
}
// 创建avPlayer实例对象
this.avPlayer = await media.createAVPlayer();
// 创建状态机变化回调函数
await this.setAVPlayerCallback((avPlayer: media.AVPlayer) => {
this.percent = avPlayer.width / avPlayer.height;
this.setVideoWH();
this.durationTime = this.getDurationTime();
setInterval(() => { // 更新当前时间
if (!this.isSwiping) {
this.currentTime = this.getCurrentTime();
}
}, SET_INTERVAL);
});
// 为fdSrc赋值触发initialized状态机上报
this.avPlayer.fdSrc = avFileDescriptor;
}
avPlay(): void {
if (this.avPlayer) {
try {
this.avPlayer.play();
} catch (e) {
console.error(`${this.tag}: avPlay = ${JSON.stringify(e)}`);
}
}
}
avPause(): void {
if (this.avPlayer) {
try {
this.avPlayer.pause();
console.info(`${this.tag}: avPause==`);
} catch (e) {
console.error(`${this.tag}: avPause== ${JSON.stringify(e)}`);
}
}
}
async avSeek(seekTime: number, mode: SliderChangeMode): Promise<void> {
if (this.avPlayer) {
try {
console.info(`${this.tag}: videoSeek seekTime== ${seekTime}`);
this.avPlayer.seek(seekTime, 2);
this.currentTime = seekTime;
} catch (e) {
console.error(`${this.tag}: videoSeek== ${JSON.stringify(e)}`);
}
}
}
avSetSpeed(speed: number): void {
if (this.avPlayer) {
try {
this.avPlayer.setSpeed(speed);
console.info(`${this.tag}: avSetSpeed enum ${speed}`);
} catch (e) {
console.error(`${this.tag}: avSetSpeed == ${JSON.stringify(e)}`);
}
}
}
// 注册avplayer回调函数
async setAVPlayerCallback(callback: (avPlayer: media.AVPlayer) => void, vType?: number): Promise<void> {
// seek操作结果回调函数
if (this.avPlayer == null) {
console.error(`${this.tag}: avPlayer has not init!`);
return;
}
this.avPlayer.on('seekDone', (seekDoneTime) => {
console.info(`${this.tag}: setAVPlayerCallback AVPlayer seek succeeded, seek time is ${seekDoneTime}`);
});
this.avPlayer.on('speedDone', (speed) => {
console.info(`${this.tag}: setAVPlayerCallback AVPlayer speedDone, speed is ${speed}`);
});
// error回调监听函数,当avPlayer在操作过程中出现错误时调用reset接口触发重置流程
this.avPlayer.on('error', (err) => {
console.error(`${this.tag}: setAVPlayerCallback Invoke avPlayer failed ${JSON.stringify(err)}`);
if (this.avPlayer == null) {
console.error(`${this.tag}: avPlayer has not init on error`);
return;
}
this.avPlayer.reset();
});
// 状态机变化回调函数
this.avPlayer.on('stateChange', async (state, reason) => {
if (this.avPlayer == null) {
console.info(`${this.tag}: avPlayer has not init on state change`);
return;
}
switch (state) {
case 'idle': // 成功调用reset接口后触发该状态机上报
console.info(`${this.tag}: setAVPlayerCallback AVPlayer state idle called.`);
break;
case 'initialized': // avplayer 设置播放源后触发该状态上报
console.info(`${this.tag}: setAVPlayerCallback AVPlayer state initialized called.`);
if (this.surfaceId) {
this.avPlayer.surfaceId = this.surfaceId; // 设置显示画面,当播放的资源为纯音频时无需设置
console.info(`${this.tag}: setAVPlayerCallback this.avPlayer.surfaceId = ${this.avPlayer.surfaceId}`);
this.avPlayer.prepare();
}
break;
case 'prepared': // prepare调用成功后上报该状态机
console.info(`${this.tag}: setAVPlayerCallback AVPlayer state prepared called.`);
this.avPlayer.on('bufferingUpdate', (infoType: media.BufferingInfoType, value: number) => {
console.info(`${this.tag}: bufferingUpdate called, infoType value: ${infoType}, value:${value}}`);
})
this.durationTime = this.avPlayer.duration;
this.currentTime = this.avPlayer.currentTime;
this.avPlayer.play(); // 调用播放接口开始播放
console.info(`${this.tag}:
setAVPlayerCallback speedSelect: ${this.speedSelect}, duration: ${this.durationTime}`);
if (this.speedSelect != -1) {
switch (this.speedSelect) {
case SPEED_ZERO:
this.avSetSpeed(media.PlaybackSpeed.SPEED_FORWARD_1_00_X);
break;
case SPEED_ONE:
this.avSetSpeed(media.PlaybackSpeed.SPEED_FORWARD_1_25_X);
break;
case SPEED_TWO:
this.avSetSpeed(media.PlaybackSpeed.SPEED_FORWARD_1_75_X);
break;
case SPEED_THREE:
this.avSetSpeed(media.PlaybackSpeed.SPEED_FORWARD_2_00_X);
break;
}
}
callback(this.avPlayer);
break;
case 'playing': // play成功调用后触发该状态机上报
console.info(`${this.tag}: setAVPlayerCallback AVPlayer state playing called.`);
if (this.intervalID != -1) {
clearInterval(this.intervalID)
}
this.intervalID = setInterval(() => { // 更新当前时间
AppStorage.setOrCreate('durationTime', this.durationTime);
AppStorage.setOrCreate('currentTime', this.currentTime);
}, 100);
let eventDataTrue: emitter.EventData = {
data: {
'flag': true
}
};
let innerEventTrue: emitter.InnerEvent = {
eventId: 2,
priority: emitter.EventPriority.HIGH
};
emitter.emit(innerEventTrue, eventDataTrue);
break;
case 'completed': // 播放结束后触发该状态机上报
console.info(`${this.tag}: setAVPlayerCallback AVPlayer state completed called.`);
let eventDataFalse: emitter.EventData = {
data: {
'flag': false
}
};
let innerEvent: emitter.InnerEvent = {
eventId: 1,
priority: emitter.EventPriority.HIGH
};
emitter.emit(innerEvent, eventDataFalse);
if (this.intervalID != -1) {
clearInterval(this.intervalID)
}
this.avPlayer.off('bufferingUpdate')
AppStorage.setOrCreate('currentTime', this.durationTime);
break;
case 'released':
console.info(`${this.tag}: setAVPlayerCallback released called.`);
break
case 'stopped':
console.info(`${this.tag}: setAVPlayerCallback AVPlayer state stopped called.`);
break
case 'error':
console.error(`${this.tag}: setAVPlayerCallback AVPlayer state error called.`);
break
case 'paused':
console.info(`${this.tag}: setAVPlayerCallback AVPlayer state paused called.`);
break
default:
console.info(`${this.tag}: setAVPlayerCallback AVPlayer state unknown called.`);
break;
}
});
// 时间上报监听函数
this.avPlayer.on('timeUpdate', (time: number) => {
this.currentTime = time;
});
}
aboutToAppear() {
this.windowWidth = display.getDefaultDisplaySync().width;
this.windowHeight = display.getDefaultDisplaySync().height;
this.surfaceW = this.windowWidth * SURFACE_W;
this.surfaceH = this.surfaceW / SURFACE_H;
this.isPaused = true;
this.context = getContext(this) as common.UIAbilityContext;
}
aboutToDisappear() {
if (this.avPlayer == null) {
console.info(`${this.tag}: avPlayer has not init aboutToDisappear`);
return;
}
this.avPlayer.release((err) => {
if (err == null) {
console.info(`${this.tag}: videoRelease release success`);
} else {
console.error(`${this.tag}: videoRelease release failed, error message is = ${JSON.stringify(err.message)}`);
}
});
emitter.off(innerEventFalse.eventId);
}
onPageHide() {
this.avPause();
this.isPaused = false;
}
onPageShow() {
emitter.on(innerEventTrue, (res: emitter.EventData) => {
if (res.data) {
this.isPaused = res.data.flag;
this.XComponentFlag = res.data.flag;
}
});
emitter.on(innerEventFalse, (res: emitter.EventData) => {
if (res.data) {
this.isPaused = res.data.flag;
}
});
emitter.on(innerEventWH, (res: emitter.EventData) => {
if (res.data) {
this.windowWidth = res.data.width;
this.windowHeight = res.data.height;
this.setVideoWH();
}
});
}
setVideoWH(): void {
if (this.percent >= 1) { // 横向视频
this.surfaceW = Math.round(this.windowWidth * PROPORTION);
this.surfaceH = Math.round(this.surfaceW / this.percent);
} else { // 纵向视频
this.surfaceH = Math.round(this.windowHeight * PROPORTION);
this.surfaceW = Math.round(this.surfaceH * this.percent);
}
}
@Builder
CoverXComponent() {
XComponent({
// 装载视频容器
id: 'xComponent',
type: XComponentType.SURFACE,
controller: this.xComponentController
})
.id('VideoView')
.visibility(this.XComponentFlag ? Visibility.Visible : Visibility.Hidden)
.onLoad(() => {
this.surfaceId = this.xComponentController.getXComponentSurfaceId();
this.avSetupVideo();
})
.height(`${this.surfaceH}px`)
.width(`${this.surfaceW}px`)
}
build() {
Column() {
Stack() {
Column() {
this.CoverXComponent()
}
.align(Alignment.TopStart)
.margin({ top: 80 })
.id('VideoView')
.justifyContent(FlexAlign.Center)
Text()
.height(`${this.surfaceH}px`)
.width(`${this.surfaceW}px`)
.margin({ top: 80 })
.backgroundColor(Color.Black)
.opacity($r('app.float.size_zero_five'))
.visibility(this.isSwiping ? Visibility.Visible : Visibility.Hidden)
Row() {
Text(this.timeConvert(this.currentTime))
.id("currentTime")
.fontSize($r('app.float.size_24'))
.opacity($r('app.float.size_1'))
.fontColor($r("app.color.slider_selected"))
Text("/" + this.timeConvert(this.durationTime))
.id("durationTime")
.fontSize($r('app.float.size_24'))
.opacity($r('app.float.size_1'))
.fontColor(Color.White)
}
.margin({ top: 80 })
.visibility(this.isSwiping ? Visibility.Visible : Visibility.Hidden)
Column() {
Blank()
Column() {
// 进度条
Row() {
Row() {
// 播放、暂停键
Image(this.isPaused ? $r("app.media.ic_video_play") : $r("app.media.ic_video_pause"))// 暂停/播放
.id(this.isPaused ? 'pause' : 'play')
.width($r('app.float.size_40'))
.height($r('app.float.size_40'))
.onClick(() => {
if (this.isPaused) {
this.avPause();
this.isPaused = false;
} else {
this.avPlay();
this.isPaused = true;
}
})
// 左侧时间
Text(this.timeConvert(this.currentTime))
.id("currentTimeText")
.fontColor(Color.White)
.textAlign(TextAlign.End)
.fontWeight(FontWeight.Regular)
.margin({ left: $r('app.float.size_10') })
}
// 进度条
Row() {
Slider({
value: this.currentTime,
min: 0,
max: this.durationTime,
style: SliderStyle.OutSet
})
.id('Slider')
.blockColor(Color.White)
.trackColor(Color.Gray)
.selectedColor($r("app.color.slider_selected"))
.showTips(false)
.onChange((value: number, mode: SliderChangeMode) => {
if (this.seekTime !== value) {
this.seekTime = value;
this.avSeek(Number.parseInt(value.toFixed(0)), mode);
}
})
}
.layoutWeight(1)
Row() {
// 右侧时间
Text(this.timeConvert(this.durationTime))
.id("durationTimeText")
.fontColor(Color.White)
.fontWeight(FontWeight.Regular)
// 倍速按钮
Button(this.speedName, { type: ButtonType.Normal })
.border({ width: $r('app.float.size_1'), color: Color.White })
.width(75)
.height($r('app.float.size_40'))
.fontSize($r('app.float.size_15'))
.borderRadius($r('app.float.size_24'))
.fontColor(Color.White)
.backgroundColor(Color.Black)
.opacity($r('app.float.size_1'))
.margin({ left: $r('app.float.size_10') })
.id('Speed')
.onClick(() => {
this.speedIndex = (this.speedIndex + 1) % SPEED_COUNT;
this.speedSelect = this.speedIndex;
this.speedName = this.speedList[this.speedIndex];
if (!this.avPlayer) {
return;
}
switch (this.speedSelect) {
case 0:
this.avSetSpeed(media.PlaybackSpeed.SPEED_FORWARD_1_00_X);
break;
case 1:
this.avSetSpeed(media.PlaybackSpeed.SPEED_FORWARD_1_25_X);
break;
case 2:
this.avSetSpeed(media.PlaybackSpeed.SPEED_FORWARD_1_75_X);
break;
case 3:
this.avSetSpeed(media.PlaybackSpeed.SPEED_FORWARD_2_00_X);
break;
}
})
}
}
.justifyContent(FlexAlign.Center)
.padding({ left: $r('app.float.size_25'), right: $r('app.float.size_30') })
.width('100%')
}
.width('100%')
.justifyContent(FlexAlign.Center)
}
.width('100%')
.height('100%')
}
.backgroundColor(Color.Black)
.height('90%')
.width('100%')
Row() {
Text(this.title)
.fontSize($r('app.float.size_20'))
.fontColor(Color.White)
.opacity($r('app.float.size_zero_six'))
.fontWeight(FontWeight.Regular)
.textAlign(TextAlign.Center)
}
}.backgroundColor(Color.Black)
.height('100%')
.width('100%')
}
}