概述
本文适用于视频播放类应用的开发,针对市场上主流视频播放类应用的常见场景,介绍了如何基于HarmonyOS能力快速实现视频播放应用。
从用户交互和音频流状态变更两个维度,指导开发者基于HarmonyOS提供的媒体和ArkUI等能力,实现视频前后台播放控制、播放形态切换、音频焦点切换、播放设备切换等场景,可以为视频播放应用提供灵活的交互体验和良好的观看效果。
场景分析
场景名称 | 子场景名称 | 描述 | 实现方案 |
---|---|---|---|
与用户交互 | [播放进度控制] | 进度条控制。 | 使用[Slider组件]实现进度条,在其onChange回调中触发进度调节。 |
手势调节播放进度。 | 通过给组件[绑定手势识别],来实现手势调节播放进度。 | ||
显示进度条弹窗。 | 给进度条添加[Popup弹窗]。 | ||
[播放形态切换] | 横竖屏切换。 | 基于[窗口能力]实现全屏播放。 | |
悬浮窗播放。 | [应用布局适配智慧多窗]。 | ||
[播控中心控制视频状态] | 应用响应播控中心通知,向播控中心同步视频信息状态。 | [监听AVSession播放状态事件]。 | |
[视频后台播放] | 应用至于后台时可以继续播放。 | 基于[AVSession Kit]和[Background Tasks Kit]申请长时任务。 | |
[滑动调节音量及亮度] | 视频全屏播放时,可以滑动屏幕调节音量及亮度。 | 使用[AVVolumePanel组件]控制音量,使用[Slider组件]及[setWindowBrightness]方法控制亮度。 | |
音频流状态变更 | [多音频并发打断] | 视频应用被其他应用的音频打断做出对应的行为。 | [AVPlayer监听音频打断事件]。 |
[播放设备切换] | 视频应用连接的播放设备状态发生变更时做出对应的行为。 | [AVPlayer监听设备音频流输出变化]。 |
与用户交互
播放进度控制
进度条控制
进度条作为视频应用的一个基础能力,可以通过点击或拖动进度条来调节视频播放进度。采用[Slider组件]实现进度条功能,根据Slider组件属性设置进度条样式,并在其onChange()事件中触发视频播放器AVPlayer的seek()方法,实现视频进度的控制。
Slider({
value: this.isSliderGesture ? this.panEndTime : this.avPlayerController.currentTime,
step: 0.1,
min: 0,
max: this.avPlayerController.durationTime,
style: this.sliderStyle
})
// ...
.trackColor($r('app.color.white_opacity_1_color')) // 滑轨背景颜色
.showSteps(false) // 是否显示步长刻度
.blockSize({ width: this.blockSize, height: this.blockSize }) // 滑块大小
.blockColor($r('sys.color.background_primary')) // 滑块颜色
.trackThickness(this.trackThicknessSize) // 滑轨粗细
.trackBorderRadius(2) // 底板圆角半径
.selectedBorderRadius(2) // 已滑动部分圆角半径
// ...
.onChange((value: number, mode: SliderChangeMode) => {
this.sliderOnchange(value, mode); // 进度条变化接口
})
sliderOnchange(seconds: number, mode: SliderChangeMode) {
let seekTime: number = seconds * this.avPlayerController.duration / this.avPlayerController.durationTime;
this.currentStringTime = secondToTime(Math.floor(seekTime / 1000));
this.avPlayerController.setCurrentStringTime(this.currentStringTime);
switch (mode) {
case SliderChangeMode.Begin:
break;
case SliderChangeMode.Click:
break;
case SliderChangeMode.Moving:
// ...
break;
case SliderChangeMode.End:
this.avPlayerController.seek(seekTime); // 调用AVPlayer的seek方法控制播放进度
// ...
break;
default:
break;
}
}
手势调节播放进度
通常视频播放应用还可以支持手势滑动来调节播放的进度,可以给组件绑定手势识别,来实现在视频界面左右滑动调节视频播放进度的能力,手势类型选择[PanGesture](平移手势)。
- onActionStart()阶段使用本地变量记录当前视频播放的位置,并更新进度条状态变量,重新渲染UI界面;
- onActionUpdate()阶段根据滑动的距离,计算滑动后视频应跳转的播放位置,并渲染UI界面进度条动效;
- onActionEnd()阶段将最终计算出的播放位置传给AVPlayer控制器,调用AVPlayer的seek()方法让视频跳转到指定播放位置,并更新进度条状态变量,重新渲染UI界面。
build() {
Stack({ alignContent: Alignment.BottomEnd }) {
// ...
XComponent({
id: 'XComponent',
type: XComponentType.SURFACE,
controller: this.xComponentController
})
// ...
}
// ...
.gesture(
PanGesture({ direction: PanDirection.Horizontal })
.onActionStart((event: GestureEvent) => {
this.isSliderGesture = true;
this.panStartX = event.offsetX;
this.panStartTime = this.avPlayerController.currentTime;
this.sliderOnchange(this.panStartTime, SliderChangeMode.Begin);
})
.onActionUpdate((event: GestureEvent) => {
this.isSliderGesture = true;
let panTime =
this.panStartTime +
(this.panStartX + event.offsetX) / this.slideWidth * this.avPlayerController.durationTime;
this.panEndTime = Math.min(Math.max(0, panTime), this.avPlayerController.durationTime);
this.sliderOnchange(this.panEndTime, SliderChangeMode.Moving);
})
.onActionEnd(() => {
this.sliderOnchange(this.panEndTime, SliderChangeMode.End);
this.isSliderGesture = false;
})
)
}
显示进度条弹窗
在正常浏览视频的过程中,应用会记录用户的浏览历史,当再次切换到原视频时,根据历史数据在进度条上以弹窗的形式显示相关信息。并且让弹窗跟随滑块位置移动,弹窗保留1秒后消失。同时历史数据的保留跟随视频组件的生命周期存亡。
由于Slider自带的showTips无法对弹窗样式进行自定义,只支持圆形气泡,且无法自定义控制弹窗的显示时长和出现时机。所以我们通过创建一个和Slider滑块大小一致的透明Stack,并计算滑块在屏幕中的位置,将计算的位置数据同步设置给Stack,并给Stack绑定Popup弹窗跟随Slider滑块运动。
关键代码:
通过给Slider组件绑定区域变化事件onAreaChange(),计算滑动位置信息。
// Slider进度条
Slider({
value: this.isSliderGesture ? this.panEndTime : this.avPlayerController.currentTime,
step: 0.1,
min: 0,
max: this.avPlayerController.durationTime,
style: this.sliderStyle
})
// ...
.blockSize({ width: this.blockSize, height: this.blockSize })
// 计算滑块位置
.onAreaChange(() => {
let videoSlider: componentUtils.ComponentInfo = componentUtils.getRectangleById('video_slider')
this.slideWidth = px2vp(videoSlider.size.width);
// 计算offsetY:Slider滑块位置的纵坐标
this.offsetY = px2vp(videoSlider.localOffset.y);
this.beginX = px2vp(videoSlider.localOffset.x);
})
// 进度条变化时最终会调用avPlayer.seek()接口
.onChange((value: number, mode: SliderChangeMode) => {
this.sliderOnchange(value, mode);// 进度条变化时会调用avPlayer.seek()接口
})
Stack透明块大小、位置设置以及给Stack绑定Popup弹窗。
// 设置和Slider大小一样的透明stack块
Stack() {
}
.backgroundColor($r('sys.color.background_primary'))
.width(this.b