往期推文全新看点(文中附带全新鸿蒙5.0全栈学习笔录)
✏️ 鸿蒙应用开发与鸿蒙系统开发哪个更有前景?
✏️ 嵌入式开发适不适合做鸿蒙南向开发?看完这篇你就了解了~
✏️ 对于大前端开发来说,转鸿蒙开发究竟是福还是祸?
✏️ 鸿蒙岗位需求突增!移动端、PC端、IoT到底该怎么选?
✏️ 记录一场鸿蒙开发岗位面试经历~
✏️ 持续更新中……
概述
本文适用于视频播放类应用的开发,针对市场上主流视频播放类应用的常见场景,介绍了如何基于HarmonyOS能力快速实现视频播放应用。
从用户交互和音频流状态变更两个维度,指导开发者基于HarmonyOS提供的媒体和ArkUI等能力,实现视频前后台播放控制、播放形态切换、音频焦点切换、播放设备切换等场景,可以为视频播放应用提供灵活的交互体验和良好的观看效果。
说明
在阅读本文之前,建议开发者先熟悉视频播放器《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.blockSize)
.height(this.blockSize)
.borderRadius($r('app.float.font_size_20'))
.visibility(this.isShowTips ? Visibility.Visible : Visibility.None)
.position({ x: this.tipsOffset, y: this.offsetY })
// 将Popup绑定到Stack块上,跟随Slider滑块运动
.bindPopup(this.isShowTips, {
builder: this.popupBuilder,
placement: Placement.Top,
mask: false,
arrowOffset: 0,
popupColor: $r('sys.color.background_primary'),
backgroundBlurStyle: BlurStyle.BACKGROUND_ULTRA_THICK,
enableArrow: true,
arrowPointPosition: ArrowPointPosition.CENTER,
radius: this.blockSize,
})
播放形态切换
横竖屏切换
横竖屏切换是视频应用常见的功能,用户会根据自己喜好选择在横屏或竖屏模式下观看。
设置窗口旋转策略有两种方式:
- 通过module.json5文件中“orientation”字段进行设置。
- 在代码中通过调用窗口window的 setPreferredOrientation() 方法进行设置。
这两种方式触发设置旋转的时机不同,module.json5文件中的字段在窗口启动时就会生效,对于应用启动时就需要设置横屏或者竖屏的应用,可以进行配置。而setPreferredOrientation()是在调用该方法时进行窗口方向的设置,用于在应用启动之后,还需要改变显示方向的场景。
本示例主要介绍通过使用窗口的setPreferredOrientation()方法来实现应用的横竖屏切换能力,下面给出具体实现方案 。
- 设置窗口旋转
当进入首页视频列表时,仅支持竖屏,要切换视频播放页面为横屏时,采用window窗口能力,通过setPreferredOrientation()将窗口显示的方向修改为横屏、竖屏的状态。在使用时,根据应用自身的旋转策略选择相应的参数。窗口显示方向类型枚举可以参考:window.Orientation。
// 根据应用自身的旋转策略选择相应的参数
setMainWindowOrientation(orientation: window.Orientation, callback?: Function): void {
// ...
this.mainWindowClass.setPreferredOrientation(orientation).then(() => {
callback?.();
}).catch((err: BusinessError) => {
hilog.error(0x0001, TAG, `Failed to set the ${orientation} of main window. Code:${err.code}, message:${err.message}`);
});
}
-
监听窗口变化
当用户手动设置窗口方向时,窗口的显示会发生变化,对应窗口的size也会发生改变,此时可以通过拿到窗口的宽高,并对宽高进行对比,判断当前显示是竖屏还是横屏状态,并利用该数据对布局进行适配。监听窗口尺寸的变化可以通过 window.on(‘windowSizeChange’) 进行实现。
// 在aboutToAppear中开启窗口尺寸监听
async aboutToAppear(): Promise<void> {
let context = getContext(this);
let windowClass = await window.getLastWindow(context);
// ...
// 注册窗口尺寸监听
this.windowUtil.registerOnWindowSizeChange((size) => {
if (size.width > size.height) {
// 横屏逻辑
this.isFullLandscapeScreen = true;
} else {
// 竖屏逻辑
this.isFullLandscapeScreen = false;
}
});
}
registerOnWindowSizeChange(callback?: (size: window.Size) => void) {
// ...
this.mainWindowClass.on('windowSizeChange', (size) => {
// 由于在手机中应用窗口真实宽度为屏幕宽度,故不需再改变宽度
AppStorage.setOrCreate('deviceHeight', size.height);
callback?.(size);
});
}
悬浮窗播放
视频应用添加悬浮窗能力,可以让用户在观看视频时,临时处理另一个任务或短时间多任务并行使用,如边看视频边浏览网页等行为。
- 声明支持悬浮窗
首先需要通过对module.json5配置文件中abilities标签下的 supportWindowMode 属性增加“floating”字段或使用缺省值以声明应用支持悬浮窗。
因为本视频应用需要支持横向和竖向悬浮窗两种能力,所以还需对abilities标签下的preferMultiWindowOrientation属性设置为landscape_auto,来标识当前UIAbility组件多窗布局方向。
"abilities": [
{
// ...
"preferMultiWindowOrientation": "landscape_auto"
}
]
- 适配悬浮窗布局
由于应用从全屏进入悬浮窗后,应用的窗口尺寸会发生变化,所以应用需要根据不同的窗口尺寸调整自身布局。可以通过窗口的 on(‘windowSizeChange’) 方法实现对窗口尺寸大小变化的监听。再根据窗口的尺寸变化,更新调整自身应用布局以实现适配。
- 在onWindowStageCreate()方法中,获取Window对象。
onWindowStageCreate(windowStage: window.WindowStage): void {
// ...
WindowUtil.getInstance().setWindowStage(windowStage);
// ...
}
- 通过getWindowProperties()方法返回值中的windowRect获取窗口尺寸,写入AppStorage中用于UI侧窗口尺寸的首次初始化赋值。
public setWindowStage(windowStage: window.WindowStage): void {
this.windowStage = windowStage;
this.windowStage.getMainWindow((err, windowClass: window.Window) => {
// ...
this.mainWindowClass = windowClass;
const properties = windowClass.getWindowProperties(); // 获取窗口信息
// ...
AppStorage.setOrCreate('deviceWidth', properties.windowRect.width); // 设置窗口宽度
AppStorage.setOrCreate('deviceHeight', properties.windowRect.height); // 设置窗口高度
// ...
});
}
- 使用on(‘windowSizeChange’)注册窗口尺寸变化时的监听,并写入AppStorage中供UI侧布局使用。
registerOnWindowSizeChange(callback?: (size: window.Size) => void) {
// ...
this.mainWindowClass.on('windowSizeChange', (size) => {
// 由于在手机中应用窗口真实宽度为屏幕宽度,故不需再改变宽度
AppStorage.setOrCreate('deviceHeight', size.height);
callback?.(size);
});
}
- UI侧通过@StorageProp绑定窗口尺寸后,AppStorage中属性key值对应的数据一旦改变,UI侧会同步修改。
@Entry
@Component
struct IndexPage {
@State isFloatWindow: boolean = false;
@StorageProp('deviceHeight') @Watch('onWindowSizeChange') deviceHeight: number = AppStorage.get<number>('deviceHeight') || 0;
// ...
}
- @Watch用于对状态变量的监听,所以窗口尺寸发生变化时,会引起组件的重新渲染,开发者可以根据最新的窗口尺寸动态调整应用布局。
// 因为悬浮窗宽高为系统默认,所以此处只根据窗口高度来处理isFloatWindow(是否显示悬浮窗)的值,用来控制界面组件的显隐
onWindowSizeChange() {
let deviceWidth = AppStorage.get<number>('deviceWidth') || 0;
// 判断是否是横屏或竖屏悬浮窗
if (this.isFullLandscapeScreen && Math.round((this.deviceHeight / deviceWidth) * 1000) / 1000 === 0.563) {
this.isFloatWindow = true;
} else if (this.isFullScreen &&
(Math.round((deviceWidth / this.deviceHeight) * 1000) / 1000 === 0.563 || Math.round((deviceWidth / this.deviceHeight) * 10) / 10 === 0.6)) {
this.isFloatWindow = true;
} else {
this.isFloatWindow = false;
}
}
build() {
// ...
VideoPlayer({
// ...
isFloatWindow: this.isFloatWindow,
// ...
})
// ...
}
播控中心控制视频状态
用户除了在视频界面控制视频状态外,还可以通过给应用接入AVSession播控中心,使用户在播控中心也能看到当前播放视频的信息,并通过播控中心直接对视频进行快进、快退、拖动进度、播放暂停、切换、调节音量等操作。同时应用内的视频状态与信息也会与播控中心相互同步,避免了只有在视频界面才能控制视频状态的单一场景 。
- 创建并激活媒体会话
在AvSessionController初始化时,通过createAVSession()创建AVSession实例并激活媒体会话,视频应用选择会话类型为video。开发者可根据应用的类型选择对应的会话。
import { avSession } from '@kit.AVSessionKit';
export class AvSessionController {
private avSession: avSession.AVSession | undefined = undefined;
public initAvSession() {
this.context = AppStorage.get('context');
if (!this.context) {
hilog.info(0x0001, TAG, "session create failed : context is undefined");
return;
}
// 创建AVSession,会话类型设置为'video'
avSession.createAVSession(this.context, "SHORT_AUDIO_SESSION", 'video').then(async (avSession) => {
this.avSession = avSession;
hilog.info(0x0001, TAG, `session create successed : sessionId : ${this.avSession.sessionId}`);
// 设置用于被播控中心拉起的UIAbility
this.setLaunchAbility();
// 激活媒体会话
this.avSession.activate();
});
}
}
- 设置媒体会话元数据
应用可以通过setAVMetadata()把会话的一些元数据信息设置给系统,从而在播控中心界面进行展示。如媒体ID(assetId)、标题(title)、播控中心显示的图片(mediaImage)、媒体时长(duration)等。
public async setAVMetadata(curSource: VideoData, duration: number) {
// ...
const imagePixMap = await ImageUtil.getPixmapFromMedia(curSource.head);
let metadata: avSession.AVMetadata = {
assetId: `${curSource.index}`, // 媒体ID
title: this.context?.resourceManager.getStringSync(curSource.name), // 标题
mediaImage: imagePixMap, // 图片的像素数据或者图片路径地址
duration: duration // 媒体时长,单位毫秒(ms)
};
if (this.avSession) {
this.avSession.setAVMetadata(metadata).then(() => { // 调用设置会话元数据接口
this.avSessionMetadata = metadata;
hilog.info(0x0001, TAG, "SetAVMetadata successfully");
}).catch((err: BusinessError) => {
hilog.error(0x0001, TAG, `SetAVMetadata BusinessError: code: ${err.code}, message: ${err.message}`);
});
}
}
- 设置用于被播控中心拉起的UIAbility
当用户操作媒体会话控制方的界面时,例如点击播控中心的卡片,可以拉起此处配置的UIAbility。将封装的WantAgent通过setLaunchAbility()配置给媒体会话。
private setLaunchAbility() {
// ...
const wantAgentInfo: wantAgent.WantAgentInfo = {
wants: [
{
bundleName: this.context.abilityInfo.bundleName, // 待启动Ability所在的应用Bundle名称
abilityName: this.context.abilityInfo.name // 待启动Ability名称
}
],
operationType: wantAgent.OperationType.START_ABILITIES, // 动作类型,START_ABILITY 表示开启多个有页面的Ability
requestCode: 0, // 使用者定义的一个私有值
wantAgentFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG] // 动作执行属性,UPDATE_PRESENT_FLAG 表示使用新的WantAgent的额外数据替换已存在的WantAgent中的额外数据
};
wantAgent.getWantAgent(wantAgentInfo).then((agent) => {
if (this.avSession) {
this.avSession.setLaunchAbility(agent);
}
});
}
- 注册播控命令事件监听
便于响应用户通过播控中心下发的播控命令,比如播放、暂停、停止、快进、快退等。同时只有设置了相关的事件回调,播控中心上对应的按钮才会亮起,否则为置灰状态。
public async setAvSessionListener() {
if (!this.avSessionController) {
return;
}
this.avSessionController.getAvSession()?.on('play', () => this.sessionPlayCallback()); // 设置播放命令监听事件
this.avSessionController.getAvSession()?.on('pause', () => this.sessionPauseCallback()); // 设置暂停命令监听事件
this.avSessionController.getAvSession()?.on('stop', () => this.sessionStopCallback()); // 设置停止命令监听事件
this.avSessionController.getAvSession()?.on('fastForward', (time?: number) => this.sessionFastForwardCallback(time)); // 设置快进命令监听事件
this.avSessionController.getAvSession()?.on('rewind', (time?: number) => this.sessionRewindCallback(time)); // 设置快退命令监听事件
this.avSessionController.getAvSession()?.on('seek', (seekTime: number) => this.sessionSeekCallback(seekTime)); // 设置跳转节点监听事件
}
- 应用状态上报播控中心
当视频状态发生改变时,需要通过setAVPlaybackState()向播控中心上报视频状态,来达到播控中心与应用的状态同步,包括播放状态(state)、播放位置(position)、当前媒体播放时长(duration)等。
private updateIsPlay(isPlay: boolean) {
if (this.curIndex !== this.curSource.index) {
return;
}
this.isPlaying = isPlay;
this.avSessionController.setAvSessionPlayState({
state: isPlay ? avSession.PlaybackState.PLAYBACK_STATE_PLAY : avSession.PlaybackState.PLAYBACK_STATE_PAUSE, //播放状态
position: { // 播放位置
elapsedTime: this.currentTime * 1000, // 已用时间,单位毫秒(ms)
updateTime: new Date().getTime() // 更新时间,单位毫秒(ms)
},
duration: this.duration // 当前媒体资源的时长
});
}
public setAvSessionPlayState(playbackState: avSession.AVPlaybackState) {
if (this.avSession) {
this.avSession.setAVPlaybackState(playbackState, (err: BusinessError) => {
if (err) {
hilog.error(0x0001, TAG, `SetAVPlaybackState BusinessError: code: ${err.code}, message: ${err.message}`);
} else {
hilog.info(0x0001, TAG, "SetAVPlaybackState successfully");
}
});
}
}
视频后台播放
用户在观看视频时,会遇到退出视频应用至后台的情况。当应用退到后台时,应用进程很快就会被挂机,所以为满足视频可以在后台持续播放的能力,需要对视频应用申请长时任务,来防止应用进程挂起。同时配合AVSession播控能力,用户可以做到在后台直接与视频应用进行交互,达到更加灵活的体验效果。
说明
只有使用了媒体会话服务(AVSession)的音视频应用,才能申请长时任务实现后台播放。
接入流程和关键代码
- 申请后台运行权限和声明后台模式
在module.json5配置文件中申请ohos.permission.KEEP_BACKGROUND_RUNNING权限和配置后台模式audioPlayback。
"requestPermissions": [
{
"name": "ohos.permission.KEEP_BACKGROUND_RUNNING",
"reason": "$string:reason_background",
"usedScene": {
"abilities": [
"EntryAbility"
],
"when": "always"
}
}
],
"abilities": [
{
"backgroundModes": [
"audioPlayback"
],
}
],
- 创建后台管理类
通过获取到的UIAbility上下文和wantAgent模块下getWantAgent()方法获取WantAgent对象,使用BackgroundTasksKit的startBackgroundRunning()、stopBackgroundRunning()方法分别申请和取消后台运行任务,长时任务类型选择AUDIO_PLAYBACK,表示视频后台播放。
import { common, wantAgent } from '@kit.AbilityKit';
import { backgroundTaskManager } from '@kit.BackgroundTasksKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
const TAG = '[BackgroundTaskManager]';
/**
* Background task tool class.
*/
export class BackgroundTaskManager {
public static startContinuousTask(context?: common.UIAbilityContext): void {
if (!context) {
return;
}
let wantAgentInfo: wantAgent.WantAgentInfo = {
wants: [
{
bundleName: context.abilityInfo.bundleName,
abilityName: context.abilityInfo.name
}
],
operationType: wantAgent.OperationType.START_ABILITY,
requestCode: 0,
wantAgentFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
};
wantAgent.getWantAgent(wantAgentInfo).then((wantAgentObj) => {
backgroundTaskManager.startBackgroundRunning(context,
backgroundTaskManager.BackgroundMode.AUDIO_PLAYBACK, wantAgentObj).then(() => {
hilog.info(0x0001, TAG, "startBackgroundRunning succeeded");
}).catch((err: BusinessError) => {
hilog.error(0x0001, TAG, `startBackgroundRunning failed Cause: ${JSON.stringify(err)}`);
});
});
}
public static stopContinuousTask(context?: common.UIAbilityContext): void {
if (!context) {
return;
}
backgroundTaskManager.stopBackgroundRunning(context).then(() => {
hilog.info(0x0001, TAG, "stopBackgroundRunning succeeded");
}).catch((err: BusinessError) => {
hilog.error(0x0001, TAG, `stopBackgroundRunning failed Cause: ${JSON.stringify(err)}`);
});
}
}
- 申请和销毁后台长时任务
在AVSession创建和释放时,分别申请和销毁后台长时任务。
public initAvSession() {
this.context = AppStorage.get('context');
// ...
avSession.createAVSession(this.context, "SHORT_AUDIO_SESSION", 'video').then(async (avSession) => {
this.avSession = avSession;
hilog.info(0x0001, TAG, `session create successed : sessionId : ${this.avSession.sessionId}`);
// 申请后台长时任务
BackgroundTaskManager.startContinuousTask(this.context);
this.setLaunchAbility();
this.avSession.activate();
});
}
async unregisterSessionListener() {
// ...
// 销毁后台长时任务
BackgroundTaskManager.stopContinuousTask(this.context);
}
滑动调节音量及亮度
在全屏播放视频的场景下,滑动调节音量及亮度是一项非常实用的功能,它允许用户在不离开视频播放界面的情况下,快速调整音量和亮度,展示音量及亮度大小,以获得更好的观影体验。该功能分为两部分,左侧滑动调整音量,右侧滑动调整亮度。
左侧滑动调节音量
在屏幕的左侧区域,使用 AVVolumePanel组件 显示系统音量面板,绑定 PanGesture 滑动手势事件,设置滑动方向为竖直方向,当Pan手势在移动过程中,上滑增加或者下滑减少音量,实现控制系统音量的功能。
- 使用 AVVolumePanel组件 设置音量面板;
Column() {
AVVolumePanel({
volumeLevel: this.volume,
volumeParameter: {
position: {
x: 150,
y: 300
}
}
})
}
.width('50%')
- 绑定PanGesture滑动手势事件。
.gesture(
PanGesture({ direction: PanDirection.Vertical })
.onActionStart(() => {
})
.onActionUpdate((event: GestureEvent) => {
if (event.fingerList[0].globalX > (820 / 2)) {
// 相对于应用窗口左上角的x轴坐标在屏幕右侧,调整亮度
this.visible = true;
let curBrightness = this.screenBrightness - vp2px(event.offsetY) / vp2px(this.screenHeight);
curBrightness = curBrightness >= 1.0 ? 1.0 : curBrightness;
curBrightness = curBrightness <= 0.0 ? 0.0 : curBrightness;
this.screenBrightness = curBrightness;
hilog.info(0x0000, 'AVPlayer', `this brightness is: ` + this.screenBrightness);
try {
this.mainWin.setWindowBrightness(this.screenBrightness, (err) => {
if (err) {
hilog.error(0x0000, 'AVPlayer', `Failed to set the brightness. Cause: ${JSON.stringify(err)}`);
return;
}
hilog.info(0x0000, 'AVPlayer', `Succeeded in setting the brightness.`);
});
} catch (exception) {
hilog.error(0x0000, 'AVPlayer', `Failed to set the brightness.`);
}
} else {
// 相对于应用窗口左上角的x轴坐标在屏幕左侧,调整音量
this.visible = false;
let curVolume = this.volume - vp2px(event.offsetY) / this.screenHeight;
curVolume = curVolume >= 15.0 ? 15.0 : curVolume;
curVolume = curVolume <= 0.0 ? 0.0 : curVolume;
this.volume = curVolume;
hilog.info(0x0000, 'AVPlayer', `this volume is: ` + this.volume);
}
})
.onActionEnd(() => {
setTimeout(() => {
this.visible = false;
}, 3000)
})
)
右侧滑动调节亮度
在屏幕的右侧区域,使用 Slider组件 设置一个亮度面板,绑定 PanGesture 滑动手势事件,设置滑动方向为竖直方向,当Pan手势在移动过程中调用 setWindowBrightness 方法,实现上滑增加或者下滑减少亮度的功能。
- 使用 Slider组件 设置亮度面板;
Column() {
Stack() {
Slider({
value: this.screenBrightness,
min: 0,
max: 1,
step: 0.1,
style: SliderStyle.NONE,
direction: Axis.Vertical,
reverse: true
})
.visibility(this.visible ? Visibility.Visible : Visibility.Hidden)
.height(160)
.selectedColor(Color.White)
.trackColor(Color.Black)
.trackThickness(40)
Image($r('app.media.sun_max_fill'))
.visibility(this.visible ? Visibility.Visible : Visibility.Hidden)
.margin({ top: 120 })
.width(20)
.height(20)
}
.margin({
top: 0,
right: 0
})
}
.alignItems(HorizontalAlign.End)
.justifyContent(FlexAlign.Center)
.padding({
right: 30,
bottom: 20
})
.height('100%')
.width('50%')
- 绑定PanGesture滑动手势事件。
.gesture(
PanGesture({ direction: PanDirection.Vertical })
.onActionStart(() => {
})
.onActionUpdate((event: GestureEvent) => {
if (event.fingerList[0].globalX > (820 / 2)) {
// 相对于应用窗口左上角的x轴坐标在屏幕右侧,调整亮度
this.visible = true;
let curBrightness = this.screenBrightness - vp2px(event.offsetY) / vp2px(this.screenHeight);
curBrightness = curBrightness >= 1.0 ? 1.0 : curBrightness;
curBrightness = curBrightness <= 0.0 ? 0.0 : curBrightness;
this.screenBrightness = curBrightness;
hilog.info(0x0000, 'AVPlayer', `this brightness is: ` + this.screenBrightness);
try {
this.mainWin.setWindowBrightness(this.screenBrightness, (err) => {
if (err) {
hilog.error(0x0000, 'AVPlayer', `Failed to set the brightness. Cause: ${JSON.stringify(err)}`);
return;
}
hilog.info(0x0000, 'AVPlayer', `Succeeded in setting the brightness.`);
});
} catch (exception) {
hilog.error(0x0000, 'AVPlayer', `Failed to set the brightness.`);
}
} else {
// 相对于应用窗口左上角的x轴坐标在屏幕左侧,调整音量
this.visible = false;
let curVolume = this.volume - vp2px(event.offsetY) / this.screenHeight;
curVolume = curVolume >= 15.0 ? 15.0 : curVolume;
curVolume = curVolume <= 0.0 ? 0.0 : curVolume;
this.volume = curVolume;
hilog.info(0x0000, 'AVPlayer', `this volume is: ` + this.volume);
}
})
.onActionEnd(() => {
setTimeout(() => {
this.visible = false;
}, 3000)
})
)
音频流状态交互
多音频并发打断
当多个音频流并发播放时,如果系统不加管控,会造成多音频流混音播放,容易让用户感到嘈杂,造成不好的用户体验。为了解决这个问题,系统预设了音频打断策略,对多音频播放的并发进行管控,只有持有音频焦点的音频流才可以正常播放,避免出现多个音频流无序并发播放的现象。当音频打断事件发生时,系统会根据预设策略,对音频流做出相应的操作,并向音频流状态变化的应用发送音频打断事件。
通过给AVPlayer设置 on(‘audioInterrupt’) 函数进行监听,当收到音频打断事件(InterruptEvent)时,应用根据其内容做出相应的处理策略。
说明
1、如果使用了AvPlayer播放视频,audioRendererInfo会被默认设置成Movie,应用无需处理。
2、使用on(‘audioInterrupt’)监听音频打断时,要注意使用的视频资源必须有音频流,否则无法触发音频打断事件。
音频打断状态图
以下分别对四种打断场景进行说明:
先暂停后恢复场景
场景描述:视频正常播放中,当后播应用音频类型为闹钟/电话/铃声时,视频暂停播放,待后播应用音频结束后,视频恢复播放。
- 当后播应用响起时,视频应用会监听到音频打断类型为InterruptForceType.INTERRUPT_FORCE(强制打断),中断提示为InterruptHint.INTERRUPT_HINT_PAUSE(音频暂停)事件,此时系统内部会自动暂停视频播放,但AVPlayer播放器的状态不会自动变为暂停,应用需要主动调用AVPlayer的暂停接口来保证状态一致。
- 当后播应用音频结束后,视频应用会监听到打断类型为InterruptForceType.INTERRUPT_SHARE(共享打断),中断提示为InterruptHint.INTERRUPT_HINT_RESUME(音频恢复)事件,此时系统不会自动恢复视频播放,应用需要在相应的事件中主动调用AVPlayer的播放接口完成恢复。
降低音量后恢复场景
场景描述:视频正常播放中,当后播应用音频类型为导航/TextReader控件朗读语音/语音助手类短语音时,视频会降低音量持续播放,待后播应用音频结束后,视频音量恢复。
- 当后播应用响起时,视频应用会监听到音频打断类型为InterruptForceType.INTERRUPT_FORCE(强制打断),中断提示为InterruptHint.INTERRUPT_HINT_DUCK(降低音量)事件,此时系统会自动降低视频音量并持续播放,应用无需处理,如果应用想实现自己的规则,可在相应的事件类型下进行处理。
- 当后播应用音频结束后,视频应用会监听到打断类型为InterruptForceType.INTERRUPT_FORCE(强制打断),中断提示为InterruptHint.INTERRUPT_HINT_UNDUCK(恢复音量)事件。此处系统会自动恢复视频音量,应用无需处理,如果应用想实现自己的规则,也可在相应的事件类型下进行处理。
停止后不恢复场景
场景描述:视频正常播放中,当后播应用音频类型为音乐/视频/VOIP时,当前视频停止播放,且后播应用音频结束后,视频不会恢复。
后播应用响起时,视频应用会监听到音频打断类型为InterruptForceType.INTERRUPT_FORCE(强制打断),中断提示为InterruptHint.INTERRUPT_HINT_STOP(音频结束)事件,此时系统内部会自动停止视频播放,但AVPlayer播放器状态不会自动改变,应用需要根据自身需求做出相应处理,如主动调用AVPlayer的暂停接口保证视频状态。
并发混音场景
场景描述:视频正常播放中,当后播应用音频类型为游戏/系统音效(锁屏/按键)时,视频会与后播应用并发播放。此场景为系统默认行为,应用无需适配。
关键代码:
private setInterruptCallback() {
if (!this.avPlayer) {
return;
}
this.avPlayer.on('audioInterrupt', async (interruptEvent: audio.InterruptEvent) => {
if (interruptEvent.forceType === audio.InterruptForceType.INTERRUPT_FORCE) {
// 强制打断类型(INTERRUPT_FORCE):音频相关处理已由系统执行,应用需更新自身状态,做相应调整
switch (interruptEvent.hintType) {
case audio.InterruptHint.INTERRUPT_HINT_PAUSE:
// 此分支表示系统已将音频流暂停(临时失去焦点),为保持状态一致,应用需切换至音频暂停状态
// 临时失去焦点:待其他音频流释放音频焦点后,本音频流会收到resume对应的音频打断事件,到时可自行继续播放
this.updateIsPlay(false);
this.pauseVideo();
break;
case audio.InterruptHint.INTERRUPT_HINT_STOP:
// 此分支表示系统已将音频流停止(永久失去焦点),为保持状态一致,应用需切换至音频暂停状态
// 永久失去焦点:后续不会再收到任何音频打断事件,若想恢复播放,需要用户主动触发。
this.updateIsPlay(false);
this.pauseVideo();
break;
case audio.InterruptHint.INTERRUPT_HINT_DUCK:
// 此分支表示系统自动将您的视频音量降低,应用无需处理
// 若应用不接受降低音量播放,可在此处选择其他处理方式,如主动暂停等
break;
case audio.InterruptHint.INTERRUPT_HINT_UNDUCK:
// 此分支表示系统已将音频音量恢复正常,应用无需处理
break;
default:
break;
}
} else if (interruptEvent.forceType === audio.InterruptForceType.INTERRUPT_SHARE) {
// 共享打断类型(INTERRUPT_SHARE):应用可自主选择执行相关操作或忽略音频打断事件
switch (interruptEvent.hintType) {
case audio.InterruptHint.INTERRUPT_HINT_RESUME:
// 此分支表示临时失去焦点后被暂停的音频流此时可以继续播放,建议应用继续播放,切换至音频播放状态
// 若应用此时不想继续播放,可以忽略此音频打断事件,不进行处理即可
// 继续播放,此处主动执行start(),以标识符变量started记录start()的执行结果
this.playVideo();
break;
default:
break;
}
}
})
}
播放设备切换
当应用使用的播放设备状态发生改变时,需要根据不同的场景及时做出相应的处理,来保证视频状态与交互体验的一致,如:视频播放状态变化、目标设备切换、视频界面的更新等。
实现方式通过对AVPlayer注册 on(‘audioOutputDeviceChangeWithInfo’) 监听事件,来感知播放设备的切换,并根据流设备的变更原因,实现对应的切换规则。
说明
1、如果没有对AVPlayer注册 on(‘audioOutputDeviceChangeWithInfo’) 监听,当识别到连接的播放设备时,音频流会自动切换到目标设备上。当播放设备断开时,播放器内部不会自动暂停。
2、如果应用想实现自己的设备切换规则,可以注册 on(‘audioOutputDeviceChangeWithInfo’) 监听,交由应用自行处理业务逻辑。
播放设备切换场景:
- 新播放设备连接
新设备上线时,系统会触发播放设备自动切换,无需应用主动处理。例如当手机使用扬声器播放视频时,连接到耳机后,会自动选择连接的耳机作为播放设备。
新设备来连接时,应用通过注册的audioOutputDeviceChangeWithInfo监听事件,获取到播放设备变更的原因为AudioStreamDeviceChangeReason.REASON_NEW_DEVICE_AVAILABLE,同时系统会默认切换到新设备上继续播放。如果应用对改场景有特殊需求,可在回调中自行处理业务逻辑。
this.avPlayer.on('audioOutputDeviceChangeWithInfo', (data: audio.AudioStreamDeviceChangeInfo) => {
if (data.changeReason === audio.AudioStreamDeviceChangeReason.REASON_NEW_DEVICE_AVAILABLE) {
hilog.info(0x0001, TAG, `Device connect: ${data.changeReason}`);
}
});
- 旧播放设备断开
当正在使用的播放设备断开时,应用可以根据使用场景选择暂停视频播放。例如:当断开连接的耳机时,视频暂停。
此时应用通过audioOutputDeviceChangeWithInfo监听事件,获取到设备变更原因为AudioStreamDeviceChangeReason.REASON_OLD_DEVICE_UNAVAILABLE,此处系统不会将播放器自动暂停,应用可以结合业务自行处理需求规则,如暂停视频播放。
this.avPlayer.on('audioOutputDeviceChangeWithInfo', (data: audio.AudioStreamDeviceChangeInfo) => {
if (data.changeReason === audio.AudioStreamDeviceChangeReason.REASON_OLD_DEVICE_UNAVAILABLE) {
hilog.info(0x0001, TAG, `Device break: ${data.changeReason}`);
this.pauseVideo();
}
});
-
用户强制选择设备
用户从界面选择切换音频流输出设备时,系统会自动选择新的设备播放,audioOutputDeviceChangeWithInfo监听事件获取到的变更原因为AudioStreamDeviceChangeReason.REASON_OVERRODE,应用结合业务自行处理。本篇示例未涉及该场景,只在此处进行说明。