在阅读本文之前,建议开发者先熟悉视频播放器《使用AVPlayer播放视频(ArkTS)》。
本文适用于视频播放类应用的开发,针对市场上主流视频播放类应用的常见场景,介绍了如何基于AVPlayer系统播放器实现视频播放应用。
本文指导开发者基于HarmonyOS提供的媒体和ArkUI等能力,实现视频播放、暂停、跳转播放、静音播放、循环播放、窗口缩放模式设置、倍速设置、音量设置等基本开发场景,可以为视频播放应用提供灵活的交互体验和良好的观看效果。
在阅读本文之前,建议开发者先熟悉视频播放器《使用AVPlayer播放视频(ArkTS)》。
场景名称 | 描述 | 实现方案 |
|---|---|---|
视频资源的加载、播放、暂停、退出等操作。 | 使用AVPlayer接口实现。 | |
滑动进度条精准跳转到指定时间进行播放。 | 使用Slider组件实现进度条,在其onChange回调中触发进度调节。 | |
点击按钮设置静音播放。 | 使用AVPlayer的setMediaMuted()方法控制静音状态。 | |
视频播放结束后会从初始位置再次播放。 | 通过在prepared状态下,设置视频播放器AVPlayer的loop属性值为true,实现视频循环播放。 | |
设置窗口缩放模式体验不同的缩放效果。 | 通过设置AVPlayer的videoScaleType属性设置窗口缩放模式。 | |
使用AVPlayer的setSpeed()方法设置倍速。通过添加按钮和弹窗实现通过按钮调节倍速;通过给组件绑定长按手势实现长按倍速。 | ||
可以滑动屏幕调节音量。 | 使用AVVolumePanel组件显示音量,通过给组件绑定手势滑动监听实现调节音量。 | |
视频下方显示字幕。 | 使用AVPlayer的addSubtitleFromFd()方法设置外挂字幕资源,并通过AVPlayer实例注册字幕回调函数on('subtitleUpdate');通过切换字幕资源并使用AVPlayer的reset()方法重置播放实现切换字幕。 |
通过AVPlayer实现核心视频播放控制能力,包括视频资源加载、播放、暂停、停止及退出等操作。
本开发指导将介绍如何使用AVPlayer开发视频播放功能,以完整地播放一个视频作为示例,实现端到端播放原始媒体资源。
播放的全流程包含:创建AVPlayer,设置播放资源和窗口,设置播放参数(音量/倍速/缩放模式),播放控制(播放/暂停/跳转/停止),重置,销毁资源。在进行应用开发的过程中,开发者可以通过AVPlayer的state属性主动获取当前状态或使用on('stateChange')方法监听状态变化。如果应用在视频播放器处于错误状态时执行操作,系统可能会抛出异常或生成其他未定义的行为。
原理详情可查看《使用AVPlayer播放视频(ArkTS)》。
进度条是视频应用的一个基础能力,可以通过点击或拖动进度条精准跳转到指定时间进行播放。

采用Slider组件实现进度条功能,根据Slider组件属性设置进度条样式,并在其onChange()事件中触发视频播放器AVPlayer的seek()方法跳转到指定播放位置,实现视频进度的控制。
/**
* Progress slider
*/
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 (mode === SliderChangeMode.Begin) {
this.isSwiping = true;
this.avPlayerController.videoPause();
}
this.avPlayerController.videoSeek(value);
this.currentTime = value;
if (mode === SliderChangeMode.End) {
this.isSwiping = false;
this.flag = true;
this.avPlayerController.videoPlay();
}
})通过界面按钮快捷切换视频播放静音状态,实现一键开启或关闭静音,提升媒体播放的交互便捷性。

通过视频播放器AVPlayer的setMediaMuted()方法,实现控制视频静音状态。
/**
* Video Muted Button
*/
Button() {
Image(this.isMuted ? $r('app.media.ic_video_speaker_slash') : $r('app.media.ic_video_speaker'))
.width($r('app.float.size_30'))
.height($r('app.float.size_30'))
}
.type(ButtonType.Normal)
.width($r('app.float.size_30'))
.height($r('app.float.size_30'))
.borderRadius($r('app.float.size_20'))
.backgroundColor('rgba(0, 0, 0, 0)')
.margin({ left: $r('app.float.size_5') })
.fontColor(Color.White)
.onClick(() => {
this.isMuted = !this.isMuted;
this.avPlayerController.videoMuted(this.isMuted)
})/**
* Video muted
* @param isMuted
* @returns
*/
async videoMuted(isMuted: boolean): Promise<void> {
if (this.avPlayer) {
try {
this.isMuted = isMuted;
await this.avPlayer!.setMediaMuted(media.MediaType.MEDIA_TYPE_AUD, isMuted)
} catch (err) {
hilog.error(CommonConstants.LOG_DOMAIN, TAG,
`videoMuted failed, code is ${err.code}, message is ${err.message}`);
}
}
}本功能可以用于在视频播放结束后自动将播放器重置至初始状态,使用户能够立即重新开始播放视频内容,实现无缝循环观看体验。
// Callback function for state machine changes
this.avPlayer.on('stateChange', async (state) => {
if (!this.avPlayer) {
return;
}
switch (state) {
// ...
case 'prepared': // This state machine is reported after the prepare interface is successfully invoked.
this.isReady = true;
this.avPlayer.loop = true
// ...
break;
// ...
}
});通过窗口缩放模式设置功能,用户可根据实际观看需求灵活调整视频内容的显示方式。该功能在未设置视频固定宽高时,在窗口尺寸频繁调整、不同宽高比视频源适配、全屏/窗口模式切换及多屏协作等场景下较为重要。
点击按钮即可弹出设置弹窗,可选择"拉伸至与窗口等大"模式,视频拉伸至与窗口等大,适合需要充分利用显示区域且对比例变化不敏感的场景;选择"缩放至最短边填满窗口"模式,视频将保持原始宽高比并以最短边为基准进行缩放,适合需要保持画面比例不变的场景。


通过设置视频播放器AVPlayer的videoScaleType属性值,实现窗口缩放模式的切换。由于VIDEO_SCALE_TYPE_SCALED_ASPECT(缩放至长边填满窗口)属性值从APIversion20开始才支持在元服务中使用,因此在这之前的版本中可根据屏幕大小为视频设置固定宽高来实现。
在未设置视频固定宽高的情况下,即未设置XComponent的height和width为固定值时,设置AVPlayer的videoScaleType属性值才能生效。
List() {
ForEach(this.scaleList, (item: Resource, index) => {
ListItem() {
Column() {
Row() {
Text(item)
// ...
Blank()
Image(this.windowScaleSelect === index ? $r('app.media.ic_radio_selected') :
$r('app.media.ic_radio'))
// ...
}
// ...
}
.width('90%')
}
.width('100%')
.height($r('app.float.size_48'))
.onClick(() => {
this.windowScaleSelect = index;
switch (this.windowScaleSelect) {
case ZERO:
this.avPlayerController.videoScaleFit();
break;
case ONE:
this.avPlayerController.videoScaleFitCrop();
break;
default:
break;
}
this.controller.close();
})
})
}/**
* Set window scale mode
*/
videoScaleFit(): void {
if (this.avPlayer) {
try {
this.avPlayer.videoScaleType = media.VideoScaleType.VIDEO_SCALE_TYPE_FIT
} catch (err) {
hilog.error(CommonConstants.LOG_DOMAIN, TAG,
`videoScaleType_0 failed, code is ${err.code}, message is ${err.message}`);
}
}
}
videoScaleFitCrop(): void {
if (this.avPlayer) {
try {
this.avPlayer.videoScaleType = media.VideoScaleType.VIDEO_SCALE_TYPE_FIT_CROP
} catch (err) {
hilog.error(CommonConstants.LOG_DOMAIN, TAG,
`videoScaleType_1 failed, code is ${err.code}, message is ${err.message}`);
}
}
}通过点击按钮选择预设倍速实现倍速设置,为用户提供灵活的视频播放速率控制。

根据选择的倍速调用视频播放器AVPlayer的setSpeed()方法设置对应值,实现视频播放倍速设置。
ForEach(this.speedList, (item: Resource, index) => {
ListItem() {
Column() {
Row() {
Text(item)
// ...
Blank()
Image(this.speedSelect === index ? $r('app.media.ic_radio_selected') :
$r('app.media.ic_radio'))
// ...
}
// ...
}
.width('90%')
}
.width('100%')
.height($r('app.float.size_48'))
.onClick(() => {
this.speedSelect = index;
switch (this.speedSelect) {
case ZERO:
this.avPlayerController.videoSpeed(media.PlaybackSpeed.SPEED_FORWARD_1_00_X);
break;
case ONE:
this.avPlayerController.videoSpeed(media.PlaybackSpeed.SPEED_FORWARD_1_25_X);
break;
case TWO:
this.avPlayerController.videoSpeed(media.PlaybackSpeed.SPEED_FORWARD_1_75_X);
break;
case THREE:
this.avPlayerController.videoSpeed(media.PlaybackSpeed.SPEED_FORWARD_2_00_X);
break;
default:
break;
}
this.controller.close();
})
}, (item: Resource, index) => index + '_' + JSON.stringify(item))
}videoSpeed(speed: number): void {
if (this.avPlayer) {
try {
this.avPlayer.setSpeed(speed);
} catch (err) {
hilog.error(CommonConstants.LOG_DOMAIN, TAG,
`videoSpeed failed, code is ${err.code}, message is ${err.message}`);
}
}
}通过长按手势实现长按屏幕时2倍速播放,长按结束时恢复1倍速播放。
通过为元素绑定长按手势事件,在长按手势开始时调用视频播放器AVPlayer的setSpeed()方法设置值为media.PlaybackSpeed.SPEED_FORWARD_2_00_X,实现长按时2倍速播放;长按结束后调用视频播放器AVPlayer的setSpeed()方法设置值为media.PlaybackSpeed.SPEED_FORWARD_1_00_X,恢复播放速度为1倍速。
.gesture(
LongPressGesture({ repeat: true })
.onAction(() => {
this.speedSelect = CASE_THREE
this.avPlayerController.videoSpeed(media.PlaybackSpeed.SPEED_FORWARD_2_00_X);
})
.onActionEnd(() => {
this.speedSelect = CASE_ZERO
this.avPlayerController.videoSpeed(media.PlaybackSpeed.SPEED_FORWARD_1_00_X);
})
)videoSpeed(speed: number): void {
if (this.avPlayer) {
try {
this.avPlayer.setSpeed(speed);
} catch (err) {
hilog.error(CommonConstants.LOG_DOMAIN, TAG,
`videoSpeed failed, code is ${err.code}, message is ${err.message}`);
}
}
}滑动调节音量是一项非常实用的功能,它允许用户在不离开视频播放界面的情况下快速调整音量,以获得更好的观看体验。该功能位于窗口左侧,通过上下滑动手势即可调整音量。

用AVVolumePanel组件显示系统音量面板,为元素绑定PanGesture滑动手势事件,设置滑动方向为竖直方向,当手势在移动时,上滑增加音量,下滑减少音量,实现控制系统音量功能。
import { AVVolumePanel } from '@kit.AudioKit';
@Component
export struct SetVolume {
@Prop volume: number = 5
@Prop volumeVisible: boolean = false
build() {
Column() {
AVVolumePanel({
volumeLevel: this.volume,
volumeParameter: {
position: {
x: 50,
y: 1000
}
}
})
.width(10)
}
.visibility(this.volumeVisible ? Visibility.Visible : Visibility.Hidden)
.height('50%')
}
}.gesture(
PanGesture({ direction: PanDirection.Vertical })
.onActionStart(() => {
})
.onActionUpdate((event: GestureEvent) => {
this.volumeVisible = true;
let curVolume = this.volume - this.getUIContext().vp2px(event.offsetY) / this.windowHeight;
curVolume = curVolume >= 15.0 ? 15.0 : curVolume;
curVolume = curVolume <= 0.0 ? 0.0 : curVolume;
this.volume = curVolume;
})
.onActionEnd(() => {
this.setVolumeTimer();
})
)在视频播放前,用户可设置外挂字幕文件,字幕将精准同步显示于视频画面下方,并可以通过按钮切换字幕语言,提升观看体验。

通过AVPlayer视频播放器的addSubtitleFromFd()方法加载外挂字幕,并使用on('subtitleUpdate')方法注册字幕回调函数。在回调函数中获取字幕文本,通过状态变量刷新Text组件显示内容。使用Text组件显示字幕并设置字体格式。
切换字幕语言需依据所选语言加载相应的字幕资源,然后调用AVPlayer的reset()方法重置播放器并重新初始化。
if (this.curSource.caption) {
let fileDescriptorSub = await this.context.resourceManager.getRawFd(this.curSource.caption);
this.avPlayer.addSubtitleFromFd(fileDescriptorSub.fd, fileDescriptorSub.offset, fileDescriptorSub.length)
.catch((err: BusinessError) => {
hilog.error(CommonConstants.LOG_DOMAIN, TAG,
`addSubtitleFromFd failed, code is ${err.code}, message is ${err.message}`);
});
}this.avPlayer.on('subtitleUpdate', (info: media.SubtitleInfo) => {
if (info) {
let text = (!info.text) ? '' : info.text;
this.currentCaption = text; //update current caption content
} else {
this.currentCaption = '';
hilog.error(CommonConstants.LOG_DOMAIN, TAG, 'subtitleUpdate info is null');
}
});Stack({ alignContent: Alignment.Center }) {
Text(this.avPlayerController.currentCaption)
.fontColor(Color.White)
.fontSize($r('app.float.size_20'))
.fontFamily('Sans')
}
.width('100%')
.position({ x: $r('app.float.size_zero'), y: $r('app.float.size_216') })
.zIndex(1)async languageChange(languageSelect: number = 0): Promise<void> {
if (this.avPlayer) {
try {
if (this.curSource && this.curSource.caption) {
this.curSource.caption = languageSelect === 0 ? 'captions.srt' : 'en_captions.srt'
this.curSource.seekTime = this.avPlayer.currentTime;
await this.avPlayer.reset();
this.initAVPlayer(this.curSource, this.surfaceID, this.avPlayer);
}
} catch (err) {
hilog.error(CommonConstants.LOG_DOMAIN, TAG,
`languageChange failed, code is ${err.code}, message is ${err.message}`);
}
}
}