IT评测·应用市场-qidao123.com
标题:
鸿蒙OS 短视频轮播结果及会话控制
[打印本页]
作者:
种地
时间:
2025-2-14 14:57
标题:
鸿蒙OS 短视频轮播结果及会话控制
代码拜见
gitee: https://gitee.com/jimmy_enjoy/hamonyos_video_demo/tree/master
结果
鸿蒙模拟器-视频轮播结果
数据定义
export interface VideoItem{
name: string
url?: string // 网址播放
video?: string // rawfile 播放
head: Resource
index: number
}
复制代码
组件
包含Swiper的父组件
Swiper(this.swiperController) {
ForEach(videoDataList, (item: VideoItem, index: number) => {
VideoPlayer({
curSource: item,
isPageShow: this.isPageShow,
curTitle: item.name,
curIndex: this.curVideoIndex,
index: index,
total: this.total,
onShowPrevious: () => this.showPrevious(index),
onShowNext: () => this.showNext(index)
})
})
}
.cachedCount(2)
// .disableSwipe(true)
.vertical(true)
.indicator(false)
.loop(false)
.curve(Curve.Ease)
.duration(300)
.onAnimationStart((index: number, targetIndex: number, extraInfo: SwiperAnimationEvent) => {
Logger.info(TAG,
`onAnimationStart index: ${index},curIndex: ${targetIndex},extraInfo: ${JSON.stringify(extraInfo)}.`);
this.curVideoIndex = targetIndex;
// this.curTitle = this.data.getData(this.curVideoIndex)?.name ?? ''
})
.width('100%')
.height('100%')
复制代码
SwiperItem 子组件
Stack({alignContent: Alignment.BottomStart}){
Column(){
XComponent({
id: 'XComponent',
type: XComponentType.SURFACE,
controller: this.xComponentController
})
.onLoad(async () => {
// this.xComponentController.setXComponentSurfaceRect({
// surfaceWidth: CommonConstants.SURFACE_WIDTH,
// surfaceHeight: CommonConstants.SURFACE_HEIGHT
// })
this.surfaceID = this.xComponentController.getXComponentSurfaceId()
hilog.info(0x0000, TAG,
'surfaceID=' + this.surfaceID + ` this.curIndex:${this.curIndex} this.index:${this.index}`);
this.initAVPlayer()
})
.aspectRatio(CommonConstants.ASPECT)
.width('100%')
.height(240)
}
.justifyContent(FlexAlign.Center)
.width(CommonConstants.WIDTH_FULL_PERCENT)
.height(CommonConstants.HEIGHT_FULL_PERCENT)
.zIndex(CommonConstants.Z_INDEX_BASE)
// 播放控制,这里需要设置更高的Z-Index,防止事件被最上层的手势监听覆盖
Column(){
this.PlayControl()
}
.zIndex(CommonConstants.Z_INDEX_MAX)
}
.gesture(
// 监听滑动手势
PanGesture({direction: PanDirection.Horizontal})
.onActionStart((event: GestureEvent) => {
this.isSliderGesture = true
this.panStartX = event.offsetX
this.panStartTime = this.currentTime
this.sliderOnchange(this.panStartTime, SliderChangeMode.Begin)
})
.onActionUpdate((event: GestureEvent) => {
this.isSliderGesture = true
let panTime = this.panStartTime +
(this.panStartX + event.offsetX) / this.slideWidth * this.durationTime
this.panEndTime = Math.min(Math.max(0, panTime), this.durationTime)
this.sliderOnchange(this.panEndTime, SliderChangeMode.Moving)
})
.onActionEnd(() => {
this.sliderOnchange(this.panEndTime, SliderChangeMode.End)
this.isSliderGesture = false
})
)
@Builder
PlayControl(){
Column(){
// 滑动时显示,当前时长/总时长
Row(){
Text(this.currentStringTime)
.fontSize($r('app.float.font_size_14'))
.fontColor(Color.White)
.opacity($r('app.float.opacity_9'))
.margin({ left: CommonConstants.TEXT_MARGIN_LEFT })
.width(CommonConstants.TEXT_LEFT_WIDTH)
.textAlign(TextAlign.End)
.zIndex(CommonConstants.SLIDER_INDEX)
Divider()
.vertical(true)
.height($r('app.float.padding_14'))
.width(CommonConstants.DIVIDER_WIDTH)
.backgroundBlurStyle(BlurStyle.Regular, { colorMode: ThemeColorMode.LIGHT })
.color(Color.White)
.opacity($r('app.float.opacity_9'))
.margin({ left: $r('app.float.margin_small'), right: $r('app.float.margin_small') })
.rotate({
x: CommonConstants.DIVIDER_X,
y: CommonConstants.DIVIDER_Y,
z: CommonConstants.DIVIDER_Z,
centerX: CommonConstants.DIVIDER_CENTER_X,
centerY: CommonConstants.DIVIDER_CENTER_Y,
angle: CommonConstants.DIVIDER_ANGLE
})
Text(this.durationStringTime)
.fontSize($r('app.float.font_size_14'))
.fontColor(Color.White)
.margin({ right: CommonConstants.TEXT_MARGIN_LEFT })
.width(CommonConstants.TEXT_LEFT_WIDTH)
.textAlign(TextAlign.Start)
.opacity($r('app.float.opacity_4'))
.zIndex(CommonConstants.SLIDER_INDEX)
}
.margin({ bottom: $r('app.float.margin_small') })
.alignItems(VerticalAlign.Center)
.opacity(this.isTimeDisplay)
Column() {
Row() {
Image(this.isPlaying ? $r('sys.media.ohos_ic_public_pause') :
$r('app.media.ic_video_menu_play'))
.width($r('app.float.size_24'))
.height($r('app.float.size_24'))
.fillColor(Color.White)
.margin({ right: $r('app.float.margin_small') })
.onClick(() => {
this.iconOnclick();
})
.visibility(
// this.isFloatWindow || this.isFullLandscapeScreen || this.isFullScreen ||
this.isSliderDragging ? Visibility.None : Visibility.Visible)
Slider({
value: this.isSliderGesture ? this.panEndTime : this.currentTime,
step: CommonConstants.SLIDER_STEP,
min: CommonConstants.SLIDER_MIN,
max: this.durationTime,
style: this.sliderStyle
})
.id('video_slider')
.height(this.isSliderDragging ? $r('app.float.side_width') : $r('app.float.size_24'))
.trackColor($r('app.color.white_opacity_1_color'))
.showSteps(false)
.blockSize({ width: this.blockSize, height: this.blockSize })
.blockColor($r('sys.color.background_primary'))
.layoutWeight(1)
.trackThickness(this.trackThicknessSize)
.trackBorderRadius(CommonConstants.TRACK_BORDER_RADIUS)
.selectedBorderRadius(CommonConstants.TRACK_BORDER_RADIUS)
.zIndex(CommonConstants.SLIDER_INDEX)
.onAreaChange(() => {
let videoSlider: componentUtils.ComponentInfo = componentUtils.getRectangleById('video_slider');
this.slideWidth = px2vp(videoSlider.size.width);
// this.offsetY = px2vp(videoSlider.localOffset.y);
// this.beginX = px2vp(videoSlider.localOffset.x);
})
.onChange((value: number, mode: SliderChangeMode) => {
this.sliderOnchange(value, mode);
})
}
.width(CommonConstants.WIDTH_FULL_PERCENT)
}
}
}
复制代码
AVPlayer
创建AVPlayer
async initAVPlayer(){
hilog.info(0x0000, TAG, 'createAVPlayer begin')
media.createAVPlayer().then((video: media.AVPlayer) => {
if(video !== null){
Logger.info(TAG, 'initAVPlayer video')
this.avPlayer = video
this.avPlayerStart(this.curSource, this.avPlayer)
this.setAVPlayerCallback(this.avPlayer)
}
}).catch((error: BusinessError) => {
Logger.error(TAG, `AVPlayer catchCallback, error message:${error.message}`);
})
}
复制代码
注册AVPlayer回调函数
setAVPlayerCallback(avPlayer: media.AVPlayer){
// 更新视频当前时间状态
avPlayer.on('timeUpdate', (time: number) => {
if(time > this.currentTime * 1000){
animateTo({duration: 1000, curve: Curve.Linear}, () => {
this.currentTime = Math.floor(time / 1000)
})
} else {
this.currentTime = Math.floor(time / 1000)
}
this.currentStringTime = secondToTime(Math.floor(time / CommonConstants.SECOND_TO_MS))
})
// 错误监听
avPlayer.on('error', (err: BusinessError) => {
hilog.error(0x0000, TAG, `Invoke avPlayer failed, code is ${err.code}, message is ${err.message}` +
`----state:${avPlayer.state} this.curIndex:${this.curIndex} this.index: ${this.index}`);
avPlayer.reset();
})
this.setAVPlayerStateListen(avPlayer)
}
setAVPlayerStateListen(avPlayer: media.AVPlayer){
// 监听状态变化
avPlayer.on('stateChange', async (state: string) => {
Logger.info(TAG, 'avplayer stateChange')
switch (state){
case 'idle': // call reset into idle state
hilog.info(0x0000, TAG, 'AVPlayer state idle called.' + ` this.curIndex:${this.curIndex} this.index:${this.index}`)
break
case 'initialized':
hilog.info(0x0000, TAG,
'AVPlayer state initialized called.' + ` this.curIndex:${this.curIndex} this.index:${this.index}`);
avPlayer.surfaceId = this.surfaceID;
avPlayer.prepare();
break;
case 'prepared':
avPlayer.audioInterruptMode = audio.InterruptMode.INDEPENDENT_MODE;
hilog.info(0x0000, TAG,
'AVPlayer state prepared called.' +
` this.curIndex:${this.curIndex} this.index:${this.index}`);
this.flag = true;
avPlayer.loop = true;
this.duration = avPlayer.duration;
this.durationTime = Math.floor(this.duration / CommonConstants.SECOND_TO_MS);
this.durationStringTime = secondToTime(this.durationTime);
if (this.curIndex === this.index) {
this.playVideo()
// this.firstFlag = false;
}
break;
case 'completed':
hilog.info(0x0000, TAG,
'AVPlayer state completed called.' + ` this.curIndex:${this.curIndex} this.index:${this.index}`);
this.isPlaying = false;
break;
case 'playing':
this.isPlaying = true;
hilog.info(0x0000, TAG,
'AVPlayer state playing called.' + ` this.curIndex:${this.curIndex} this.index:${this.index}` +
', source=' + this.curSource);
break;
case 'paused':
this.isPlaying = false
Logger.info(TAG,
'AVPlayer state paused called.' + ` this.curIndex:${this.curIndex} this.index:${this.index}`);
break;
case 'stopped':
Logger.info(TAG,
'AVPlayer state stopped called.' + ` this.curIndex:${this.curIndex} this.index:${this.index}`);
break;
case 'released':
Logger.info(TAG,
'AVPlayer state released called.' + ` this.curIndex:${this.curIndex} this.index:${this.index}`);
break;
case 'error':
Logger.info(TAG, 'AVPlayer state error called' + ` this.curIndex:${this.curIndex} this.index:${this.index}`);
// avPlayer.reset();
break;
default:
Logger.info(TAG,
'AVPlayer state unknown called.' + ` this.curIndex:${this.curIndex} this.index:${this.index}`);
break;
}
})
}
复制代码
AVSession会话控制
需要申请长时使命
在通知栏可以由系统视频控制器举行视频的控制,包括进度条滑动、快进、快退、暂停、下一首、上一首。
后台长时使命申请
需要在module.json中注册
{
"name": "ohos.permission.KEEP_BACKGROUND_RUNNING",
"reason": "$string:reason_background",
"usedScene": {
"abilities": [
"EntryAbility"
],
"when": "always"
}
}
复制代码
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
}
],
actionType: wantAgent.OperationType.START_ABILITY,
requestCode: 0,
actionFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
}
wantAgent.getWantAgent(wantAgentInfo).then((wantAgentObj) => {
try {
// 启动后台任务
backgroundTaskManager.startBackgroundRunning(context, backgroundTaskManager.BackgroundMode.AUDIO_PLAYBACK,
wantAgentObj).then(() => {
Logger.info(TAG, 'startBackgroundRunning succeeded');
}).catch((err: BusinessError) => {
Logger.error(TAG, `startBackgroundRunning failed Cause: ${JSON.stringify(err)}`);
})
}catch (error){
Logger.error(TAG, `Operation startBackgroundRunning failed. code is ${error.code} message is ${error.message}`);
}
})
}
public static stopContinuousTask(context?: common.UIAbilityContext){
try {
if (!context) {
return
}
backgroundTaskManager.stopBackgroundRunning(context).then(() => {
Logger.info(TAG, 'stopBackgroundRunning succeeded');
}).catch((err: BusinessError) => {
Logger.error(TAG, `stopBackgroundRunning failed Cause: ${JSON.stringify(err)}`);
});
} catch (error) {
Logger.error(TAG, `stopBackgroundRunning failed. code is ${error.code} message is ${error.message}`);
}
}
}
复制代码
AVSession
AVSession 在整个应用中,全局共享实例。
创建
export class AvSessionController{
private static instance: AvSessionController | null
private context: common.UIAbilityContext | undefined = undefined;
private avSession: avSession.AVSession | undefined = undefined;
private avSessionMetadata: avSession.AVMetadata | undefined = undefined;
private constructor() {
this.initAvSession()
}
public getAvSession() {
return this.avSession;
}
public getAvSessionMetadata() {
return this.avSessionMetadata;
}
public static getInstance(): AvSessionController{
if(!AvSessionController.instance){
AvSessionController.instance = new AvSessionController()
}
return AvSessionController.instance
}
// 初始化
public initAvSession(){
this.context = AppStorage.get('context')
if(!this.context){
Logger.info(TAG, `session create failed : conext is undefined`)
return
}
avSession.createAVSession(this.context, "SHORT_AUDIO_SESSION", 'video').then(async (avSession) => {
this.avSession = avSession
Logger.info(TAG, `session create successed : sessionId : ${this.avSession.sessionId}`);
// 启动后台长任务
BackgroundTaskManager.startContinuousTask(this.context);
// 设置点击后启动的activity
this.setLaunchAbility();
this.avSession.activate();
})
}
private setLaunchAbility(){
if(!this.context) return
const wantAgentInfo: wantAgent.WantAgentInfo = {
wants: [
{
bundleName: this.context.abilityInfo.bundleName,
abilityName: this.context.abilityInfo.name
}
],
actionType: wantAgent.OperationType.START_ABILITIES,
requestCode: 0,
actionFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
}
wantAgent.getWantAgent(wantAgentInfo).then((agent) => {
if(this.avSession) this.avSession.setLaunchAbility(agent)
})
}
复制代码
在AVPlayer中结合AVSession
初始化
async initAVPlayer(){
hilog.info(0x0000, TAG, 'createAVPlayer begin')
media.createAVPlayer().then((video: media.AVPlayer) => {
if(video !== null){
// ...
if(this.curIndex === this.curSource.index){
// 设置媒体显示信息
this.avSessionController.setAVMetadata(this.curSource, 0)
}
this.setAvSessionListener()
}
}).catch((error: BusinessError) => {
Logger.error(TAG, `AVPlayer catchCallback, error message:${error.message}`);
})
}
// 设置会话控制的监听,进行相应媒体播放器调整
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))
if(this.index < this.total - 1){
this.avSessionController.getAvSession()?.on('playNext', () => this.sessionPlayNextCallback())
}else {
this.avSessionController.getAvSession()?.off('playNext')
}
if(this.index > 0){
this.avSessionController.getAvSession()?.on('playPrevious', () => this.sessionPlayPreviousCallback())
}else {
this.avSessionController.getAvSession()?.off('playPrevious')
}
}
复制代码
上一首和下一首实现
通过父容器传递的showNext 和 showPrevious,具体为通过SwiperController实现切换
showNext(index: number): void{
if(this.swiperController && index < this.total - 1){
this.swiperController.changeIndex(index + 1, true)
getContext(this).eventHub.emit('videoShow', index + 1)
}
}
showPrevious(index: number): void{
if(this.swiperController && index > 0){
this.swiperController.changeIndex(index - 1, true)
getContext(this).eventHub.emit('videoShow', index + 1)
}
}
复制代码
重要公共方法实现
// 播放视频
playVideo(){
if(this.avPlayer && this.curIndex === this.index){
if(this.avPlayer.state != AVPlayerState.PREPARED && this.avPlayer.state != AVPlayerState.PAUSED &&
this.avPlayer.state != AVPlayerState.COMPLETED){
return
}
// 重置会话监听状态
this.setAvSessionListener()
this.avSessionController.setAVMetadata(this.curSource, this.duration)
this.updateIsPlay(true)
this.avPlayer.play((err: BusinessError) => {
if(err){
this.updateIsPlay(false)
Logger.error(TAG, `playVideo failed, code is ${err.code}, message is ${err.message}`);
} else {
Logger.info(TAG, `playVideo success , this.curIndex:${this.curIndex}`);
}
})
}
}
// 更新播放状态
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, // 已播放时长
updateTime: new Date().getTime() // 更新时间
},
duration: this.duration
})
}
复制代码
注
完备代码
可以用LazyForEach结合Swiper的cachecount实现懒加载与缓存。
这里没有实现屏幕旋转的环境。
更完备的示例可以参考 鸿蒙官方示例
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
欢迎光临 IT评测·应用市场-qidao123.com (https://dis.qidao123.com/)
Powered by Discuz! X3.4