一,定义
avPlayer是鸿蒙提供的开发音视频的组件,播放的流程如下:
监听事件如下:
事件类型 | 说明 |
---|---|
stateChange | 必要事件,监听播放器的state属性改变。 |
error | 必要事件,监听播放器的错误信息。 |
durationUpdate | 用于进度条,监听进度条长度,刷新资源时长。 |
timeUpdate | 用于进度条,监听进度条当前位置,刷新当前时间。 |
seekDone | 响应API调用,监听seek()请求完成情况。 当使用seek()跳转到指定播放位置后,如果seek操作成功,将上报该事件。 |
speedDone | 响应API调用,监听setSpeed()请求完成情况。 当使用setSpeed()设置播放倍速后,如果setSpeed操作成功,将上报该事件。 |
volumeChange | 响应API调用,监听setVolume()请求完成情况。 当使用setVolume()调节播放音量后,如果setVolume操作成功,将上报该事件。 |
bufferingUpdate | 用于网络播放,监听网络播放缓冲信息,用于上报缓冲百分比以及缓存播放进度。 |
audioInterrupt | 监听音频焦点切换信息,搭配属性audioInterruptMode使用。 如果当前设备存在多个音频正在播放,音频焦点被切换(即播放其他媒体如通话等)时将上报该事件,应用可以及时处理。 |
二,绘制播放器UI
先看效果图:
创建一个自定义MusicPlayer组件:
import media from '@ohos.multimedia.media'
import audio from '@ohos.multimedia.audio'
import VolumeUtils from './VolumeUtils'
import { ErrorCallback } from '@ohos.base'@Component
export default struct MusicPlayer {@State volumeImg: string = 'image/pic_volume.png'@State startImg: string = 'image/pic_play.png'@Builder bg() {Rect({ width: "100%", height: "100%" }).radius([[5, 5], [5, 5], [5, 5], [5, 5]]).fill("#F1F111")}aboutToAppear(){}aboutToDisappear(){}build() {RelativeContainer() {Image(this.startImg).width(22.5).height(22.5).alignRules({center: { anchor: '__container__', align: VerticalAlign.Center },left: { anchor: '__container__', align: HorizontalAlign.Start }}).margin({ left: 30 }).id('img_play').onClick( event =>{})Slider({value: this.progressValue,min: 0,max: 100,style: SliderStyle.OutSet }).showTips(false).onChange((value: number, mode: SliderChangeMode) => {}).width('60%').height(4).alignRules({center: { anchor: '__container__', align: VerticalAlign.Center },left: { anchor: 'img_play', align: HorizontalAlign.End }}).margin({ left: 3}).id('pg_pc')Image(this.volumeImg).width(20).height(20).alignRules({center: { anchor: '__container__', align: VerticalAlign.Center },left: { anchor: 'pg_pc', align: HorizontalAlign.End }}).margin({ left: 5 }).id('img_volume')Slider({value: this.valumeValue,min: 0,max: 15,style: SliderStyle.OutSet }).showTips(false).onChange((value: number, mode: SliderChangeMode) => {console.info('value:' + value + 'mode:' + mode.toString())}).width('20%').height(4).alignRules({center: { anchor: '__container__', align: VerticalAlign.Center },left: { anchor: 'img_volume', align: HorizontalAlign.End }}).margin({ left: 3}).id('pg_volume')Text(this.totalDution).fontSize(12).fontColor('#66000000').alignRules({top: { anchor: 'pg_pc', align: VerticalAlign.Bottom },right: { anchor: 'pg_pc', align: HorizontalAlign.End }}).margin({right: 5,top: 3}).id('txt_end_time')Text('/').fontSize(12).fontColor('#66000000').alignRules({top: { anchor: 'pg_pc', align: VerticalAlign.Bottom },right: { anchor: 'txt_end_time', align: HorizontalAlign.Start }}).margin({top: 3}).id('txt_middle_time')Text(this.currentDution).fontSize(12).fontColor('#66000000').alignRules({top: { anchor: 'pg_pc', align: VerticalAlign.Bottom },right: { anchor: 'txt_middle_time', align: HorizontalAlign.Start }}).margin({top: 3}).id('txt_start_time')}.background(this.bg()).width('100%').height(50)}
}
布局组件使用RelativeContainer,参考文章:鸿蒙Harmony-相对布局(RelativeContainer)详解_鸿蒙 relativecontainer-CSDN博客
进度条组件使用Slider,相比于Progress,Slider组件可以拖拽,可以设置滑块样式,使用起来更加方便。
三,播放音乐
要播放音乐,首先需要创建avPlayer:
createAudioPlayer(): Promise<media.AVPlayer> {return new Promise((resolve, reject) => {media.createAVPlayer().then((audio) => {resolve(audio)}).catch((error:Error)=>{reject(error)})})}
创建完了之后,avPlayer就进入了idle状态,此时可以设置播放器的Url等资源。
this.createAudioPlayer().then((audio) => {this.avPlayer = audiothis.avPlayer!!.url =this.url}).catch((error:Error)=>{console.error("MusicPlayer:播放器创建失败:"+error.message)})
设置了资源之后,avPlayer进入initialized状态,此时不可以直接播放,通过上面的流程图可以看出来,需要先准备后才可以播放。
prepare() : Promise<boolean> {return new Promise((resolve, reject) => {this.avPlayer?.prepare().then(() => {resolve(true)}).catch((error:Error) => {reject(error)})})}
准备完了之后,就进入到了prepared状态,此时调用play就可以播放了。
play(): Promise<boolean> {return new Promise((resolve, reject) => {this.avPlayer?.play().then(() => {resolve(true)}).catch((error:Error) => {reject(error)})})}
下面我们来简易的做一个播放流程,在aboutToAppear生命周期创建avPlayer,并且设置播放器的Url,在点击播放按钮的时候,准备并播放。
import media from '@ohos.multimedia.media'
import audio from '@ohos.multimedia.audio'
import VolumeUtils from './VolumeUtils'
import { ErrorCallback } from '@ohos.base'@Component
export default struct MusicPlayer {@State url:string ="" //外部传递的UrlavPlayer: media.AVPlayer | undefined = undefined //播放器@State volumeImg: string = 'image/pic_volume.png'@State startImg: string = 'image/pic_play.png'@Builder bg() {Rect({ width: "100%", height: "100%" }).radius([[5, 5], [5, 5], [5, 5], [5, 5]]).fill("#F1F111")}//创建createAudioPlayer(): Promise<media.AVPlayer> {return new Promise((resolve, reject) => {media.createAVPlayer().then((audio) => {resolve(audio)}).catch((error:Error)=>{reject(error)})})}//准备prepare() : Promise<boolean> {return new Promise((resolve, reject) => {this.avPlayer?.prepare().then(() => {resolve(true)}).catch((error:Error) => {reject(error)})})}//播放play(): Promise<boolean> {return new Promise((resolve, reject) => {this.avPlayer?.play().then(() => {resolve(true)}).catch((error:Error) => {reject(error)})})}aboutToAppear(){this.initVoice()}initVoice(){this.createAudioPlayer().then((audio) => {this.avPlayer = audiothis.avPlayer!!.url =this.url}).catch((error:Error)=>{console.error("MusicPlayer:播放器创建失败:"+error.message)})}startEvent(){this.prepare().then(value=>{this.play().then(result=>{console.error("MusicPlayer:播放成功")}).catch((err:Error)=>{console.error("MusicPlayer:点击播放失败:" + err)})}).catch((err:Error)=>{console.error("MusicPlayer:准备失败:" + err.message)}) }aboutToDisappear(){}build() {RelativeContainer() {Image(this.startImg).width(22.5).height(22.5).alignRules({center: { anchor: '__container__', align: VerticalAlign.Center },left: { anchor: '__container__', align: HorizontalAlign.Start }}).margin({ left: 30 }).id('img_play').onClick( event =>{this.startEvent()})Slider({value: this.progressValue,min: 0,max: 100,style: SliderStyle.OutSet }).showTips(false).onChange((value: number, mode: SliderChangeMode) => {}).width('60%').height(4).alignRules({center: { anchor: '__container__', align: VerticalAlign.Center },left: { anchor: 'img_play', align: HorizontalAlign.End }}).margin({ left: 3}).id('pg_pc')Image(this.volumeImg).width(20).height(20).alignRules({center: { anchor: '__container__', align: VerticalAlign.Center },left: { anchor: 'pg_pc', align: HorizontalAlign.End }}).margin({ left: 5 }).id('img_volume')Slider({value: this.valumeValue,min: 0,max: 15,style: SliderStyle.OutSet }).showTips(false).onChange((value: number, mode: SliderChangeMode) => {console.info('value:' + value + 'mode:' + mode.toString())}).width('20%').height(4).alignRules({center: { anchor: '__container__', align: VerticalAlign.Center },left: { anchor: 'img_volume', align: HorizontalAlign.End }}).margin({ left: 3}).id('pg_volume')Text(this.totalDution).fontSize(12).fontColor('#66000000').alignRules({top: { anchor: 'pg_pc', align: VerticalAlign.Bottom },right: { anchor: 'pg_pc', align: HorizontalAlign.End }}).margin({right: 5,top: 3}).id('txt_end_time')Text('/').fontSize(12).fontColor('#66000000').alignRules({top: { anchor: 'pg_pc', align: VerticalAlign.Bottom },right: { anchor: 'txt_end_time', align: HorizontalAlign.Start }}).margin({top: 3}).id('txt_middle_time')Text(this.currentDution).fontSize(12).fontColor('#66000000').alignRules({top: { anchor: 'pg_pc', align: VerticalAlign.Bottom },right: { anchor: 'txt_middle_time', align: HorizontalAlign.Start }}).margin({top: 3}).id('txt_start_time')}.background(this.bg()).width('100%').height(50)}
}
此时点击播放按钮就可以播放了。
四,暂停播放
调用暂停播放,进入暂停状态:
pausePlay(){this.pause().then(value=>{if(value){//图标更改为暂停图标this.startImg ='image/pic_play.png'}else {console.error("MusicPlayer---暂停播放失败:"+value)}}).catch((err:Error)=>{console.error("MusicPlayer---暂停播放失败:"+err.message)})}
监听播放器状态:
this.avPlayer?.on('stateChange',(state)=>{switch (state) {case "initialized":console.info('MusicPlayer-----播放器状态:initialized');this.prepare()breakcase "prepared":console.info('MusicPlayer-----播放器状态:prepared');breakcase 'playing':console.info('MusicPlayer-----播放器状态:playing');break;case 'paused':console.info('MusicPlayer-----播放器状态:paused');break;case 'completed':console.info('MusicPlayer-----播放器状态:completed');break;case 'stopped':console.info('MusicPlayer-----播放器状态:stopped');break;case 'released':console.info('MusicPlayer-----播放器状态:released');break;case 'error':console.info('MusicPlayer-----播放器状态:error');break;default:console.info('MusicPlayer-----播放器状态:default:' + state);break;}
})
开始播放时更改图标:
startPlay(){this.play().then(value=>{if(value){//图标更改为播放图标this.startImg ='image/pic_pause.png'}else {console.error("MusicPlayer---开始播放失败:"+value)}}).catch((err:Error)=>{console.error("MusicPlayer---开始播放失败:"+err.message)})
}
当还没有准备时先准备再播放:
prepareAndPlay(){this.prepare().then(value=>{this.play().then(result=>{if(result){//图标更改为播放图标this.startImg ='image/pic_pause.png'}}).catch((err:Error)=>{console.error("MusicPlayer---点击播放失败:" + err)})}).catch((err:Error)=>{console.error("MusicPlayer---准备失败:" + err.message)})
}
更改开始播放按钮点击事件:
startEvent(){switch (this.avPlayer?.state) {case 'playing'://正在播放 点击暂停播放this.pausePlay()breakcase 'paused'://暂停状态 直接开始播放this.startPlay()breakcase 'prepared'://准备状态 开始播放this.startPlay()breakcase 'initialized'://初始化状态 先准备再播放this.prepareAndPlay()breakcase 'completed'://播放完成状态 直接播放this.startPlay()breakcase 'stopped'://停止状态 先准备再播放this.prepareAndPlay()breakcase 'error'://错误状态this.prepareAndPlay()break}
}
此时播放和暂停就可以了。
五,控制进度条
定义当前时间和总时间:
@State currentDution: string = "00:00"
@State totalDution: string ="00:00"
监听获取总时长:
this.avPlayer?.on("durationUpdate",totalTime=>{this.totalTime =totalTimethis.totalDution =this.secondsToHMS(Math.floor(totalTime/1000))
})
监听当前播放时长:
this.avPlayer?.on("timeUpdate",time=>{this.currentDution =this.secondsToHMS(Math.floor(time/1000))this.progressValue =(time*100)/this.totalTime
})
转换方法:
secondsToHMS(seconds:number):string {let hours = Math.floor(seconds / 3600);let minutes = Math.floor((seconds % 3600) / 60);let secs = seconds % 60;let result =""if(hours==0){result =minutes.toString().padStart(2, '0')+":"+secs.toString().padStart(2, '0')}else {result = hours.toString().padStart(2, '0')+":"+minutes.toString().padStart(2, '0')+":"+secs.toString().padStart(2, '0')}return result
}
跳转进度:
seek( position: number) {this.avPlayer?.seek(position)
}
当进度条改变时,跳转相应位置:
Slider({value: this.progressValue,min: 0,max: 100,style: SliderStyle.OutSet }).showTips(false).onChange((value: number, mode: SliderChangeMode) => {console.error("MusicPlayer---进度条改变:"+value+"---mode:"+mode)switch (mode){case SliderChangeMode.End:const seek =(this.totalTime*value)/100this.seek(seek)breakcase SliderChangeMode.Begin:breakcase SliderChangeMode.Moving:breakcase SliderChangeMode.Click:const clickSeek =(this.totalTime*value)/100this.seek(clickSeek)break}})
六,完整代码
import media from '@ohos.multimedia.media'
import audio from '@ohos.multimedia.audio'
import VolumeUtils from './VolumeUtils'
import { ErrorCallback } from '@ohos.base'@Component
export default struct MusicPlayer {@State progressValue: number = 0@State valumeValue: number = 0@State currentDution: string = "00:00"@State totalDution: string ="00:00"avPlayer: media.AVPlayer | undefined = undefined@State url:string =""@State volumeImg: string = 'image/pic_volume.png'@State startImg: string = 'image/pic_play.png'@State totalTime: number = 0@Builder bg() {Rect({ width: "100%", height: "100%" }).radius([[5, 5], [5, 5], [5, 5], [5, 5]]).fill("#F1F111")}createAudioPlayer(): Promise<media.AVPlayer> {return new Promise((resolve, reject) => {media.createAVPlayer().then((audio) => {resolve(audio)}).catch((error:Error)=>{reject(error)})})}prepare() : Promise<boolean> {return new Promise((resolve, reject) => {this.avPlayer?.prepare().then(() => {resolve(true)}).catch((error:Error) => {reject(error)})})}play(): Promise<boolean> {return new Promise((resolve, reject) => {this.avPlayer?.play().then(() => {resolve(true)}).catch((error:Error) => {reject(error)})})}pause() : Promise<boolean> {return new Promise((resolve, reject) => {this.avPlayer?.pause().then(() => {resolve(true)}).catch((error:Error) => {reject(error)})})}stop(): Promise<boolean> {return new Promise((resolve, reject) => {this.avPlayer?.stop().then(() => {resolve(true)}).catch((error:Error) => {reject(error)})})}/*** 在音频播放器中寻找指定的位置。** @param {number} position - 要寻找到的位置。* @returns {void}*/seek( position: number) {this.avPlayer?.seek(position)}release() : Promise<boolean> {return new Promise((resolve, reject) => {this.avPlayer?.release().then(() => {resolve(true)}).catch((error:Error) => {reject(error)})})}onError(callback: ErrorCallback) {this.avPlayer?.on("error", callback)}aboutToAppear(){console.info("yhMusicPlayer:url="+this.url)this.initVoice()VolumeUtils.getVolume(audio.AudioVolumeType.MEDIA).then(value=>{this.valumeValue = value}).catch((err: Error) => {console.info("yhMusicPlayer:获取媒体音量失败"+err)})}getPlayerState():string{if(this.avPlayer){return this.avPlayer?.state}else {return 'not init'}}initVoice(){this.createAudioPlayer().then((audio) => {this.avPlayer = audiothis.avPlayer!!.url =this.urlthis.avPlayer?.on('stateChange',(state)=>{switch (state) {case "initialized":console.info('yhMusicPlayer-----播放器状态:initialized');this.prepare()breakcase "prepared":console.info('yhMusicPlayer-----播放器状态:prepared');breakcase 'playing':console.info('yhMusicPlayer-----播放器状态:playing');break;case 'paused':console.info('yhMusicPlayer-----播放器状态:paused');break;case 'completed':console.info('yhMusicPlayer-----播放器状态:completed');break;case 'stopped':console.info('yhMusicPlayer-----播放器状态:stopped');break;case 'released':console.info('yhMusicPlayer-----播放器状态:released');break;case 'error':console.info('yhMusicPlayer-----播放器状态:error');break;default:console.info('yhMusicPlayer-----播放器状态:default:' + state);break;}})this.avPlayer?.on("error", (err)=>{console.error("yhMusicPlayer-------播放器状态失败:"+err.message)})this.avPlayer?.on("durationUpdate",totalTime=>{this.totalTime =totalTimethis.totalDution =this.secondsToHMS(Math.floor(totalTime/1000))})this.avPlayer?.on("timeUpdate",time=>{this.currentDution =this.secondsToHMS(Math.floor(time/1000))this.progressValue =(time*100)/this.totalTime})}).catch((error:Error)=>{console.error("yhMusicPlayer-------播放器创建失败:"+error.message)})}secondsToHMS(seconds:number):string {let hours = Math.floor(seconds / 3600);let minutes = Math.floor((seconds % 3600) / 60);let secs = seconds % 60;let result =""if(hours==0){result =minutes.toString().padStart(2, '0')+":"+secs.toString().padStart(2, '0')}else {result = hours.toString().padStart(2, '0')+":"+minutes.toString().padStart(2, '0')+":"+secs.toString().padStart(2, '0')}return result}startPlay(){this.play().then(value=>{if(value){//图标更改为播放图标this.startImg ='image/pic_pause.png'}else {console.error("yhMusicPlayer---开始播放失败:"+value)}}).catch((err:Error)=>{console.error("yhMusicPlayer---开始播放失败:"+err.message)})}prepareAndPlay(){this.prepare().then(value=>{this.play().then(result=>{if(result){//图标更改为播放图标this.startImg ='image/pic_pause.png'}}).catch((err:Error)=>{console.error("yhMusicPlayer---点击播放失败:" + err)})}).catch((err:Error)=>{console.error("yhMusicPlayer---准备失败:" + err.message)})}pausePlay(){this.pause().then(value=>{if(value){//图标更改为暂停图标this.startImg ='image/pic_play.png'}else {console.error("yhMusicPlayer---暂停播放失败:"+value)}}).catch((err:Error)=>{console.error("yhMusicPlayer---暂停播放失败:"+err.message)})}startEvent(){switch (this.avPlayer?.state) {case 'playing'://正在播放 点击暂停播放this.pausePlay()breakcase 'paused'://暂停状态 直接开始播放this.startPlay()breakcase 'prepared'://准备状态 开始播放this.startPlay()breakcase 'initialized'://初始化状态 先准备再播放this.prepareAndPlay()breakcase 'completed'://播放完成状态 直接播放this.startPlay()breakcase 'stopped'://停止状态 先准备再播放this.prepareAndPlay()breakcase 'error'://错误状态this.prepareAndPlay()break}}aboutToDisappear(){}build() {RelativeContainer() {Image(this.startImg).width(22.5).height(22.5).alignRules({center: { anchor: '__container__', align: VerticalAlign.Center },left: { anchor: '__container__', align: HorizontalAlign.Start }}).margin({ left: 30 }).id('img_play').onClick( event =>{this.startEvent()})Slider({value: this.progressValue,min: 0,max: 100,style: SliderStyle.OutSet }).showTips(false).onChange((value: number, mode: SliderChangeMode) => {console.error("yhMusicPlayer---进度条改变:"+value+"---mode:"+mode)switch (mode){case SliderChangeMode.End:const seek =(this.totalTime*value)/100this.seek(seek)breakcase SliderChangeMode.Begin:breakcase SliderChangeMode.Moving:breakcase SliderChangeMode.Click:const clickSeek =(this.totalTime*value)/100this.seek(clickSeek)break}}).width('60%').height(4).alignRules({center: { anchor: '__container__', align: VerticalAlign.Center },left: { anchor: 'img_play', align: HorizontalAlign.End }}).margin({ left: 3}).id('pg_pc')Image(this.volumeImg).width(20).height(20).alignRules({center: { anchor: '__container__', align: VerticalAlign.Center },left: { anchor: 'pg_pc', align: HorizontalAlign.End }}).margin({ left: 5 }).id('img_volume')Slider({value: this.valumeValue,min: 0,max: 15,style: SliderStyle.OutSet }).showTips(false).onChange((value: number, mode: SliderChangeMode) => {switch (mode){case SliderChangeMode.End:VolumeUtils.setVolume(audio.AudioVolumeType.MEDIA,value).then(value=>{console.info("yhMusicPlayer音量调整成功")}).catch((err:Error)=>{console.info("yhMusicPlayer音量调整失败:"+err.message)})breakcase SliderChangeMode.Begin:breakcase SliderChangeMode.Moving:breakcase SliderChangeMode.Click:VolumeUtils.setVolume(audio.AudioVolumeType.MEDIA,value).then(value=>{console.info("yhMusicPlayer音量调整成功")}).catch((err:Error)=>{console.info("yhMusicPlayer音量调整失败:"+err.message)})break}console.info('value:' + value + 'mode:' + mode.toString())}).width('20%').height(4).alignRules({center: { anchor: '__container__', align: VerticalAlign.Center },left: { anchor: 'img_volume', align: HorizontalAlign.End }}).margin({ left: 3}).id('pg_volume')Text(this.totalDution).fontSize(12).fontColor('#66000000').alignRules({top: { anchor: 'pg_pc', align: VerticalAlign.Bottom },right: { anchor: 'pg_pc', align: HorizontalAlign.End }}).margin({right: 5,top: 3}).id('txt_end_time')Text('/').fontSize(12).fontColor('#66000000').alignRules({top: { anchor: 'pg_pc', align: VerticalAlign.Bottom },right: { anchor: 'txt_end_time', align: HorizontalAlign.Start }}).margin({top: 3}).id('txt_middle_time')Text(this.currentDution).fontSize(12).fontColor('#66000000').alignRules({top: { anchor: 'pg_pc', align: VerticalAlign.Bottom },right: { anchor: 'txt_middle_time', align: HorizontalAlign.Start }}).margin({top: 3}).id('txt_start_time')}.background(this.bg()).width('100%').height(50)}
}
使用:
@Entry
@Component
struct Index {aboutToAppear(){}@Builder bg() {Polygon({width: "100%", height: "100%"}).points([[100, 0], [0, 100], [40, 200], [160, 200], [200, 100]]).fill("#ff1122").stroke("#000000").strokeWidth(10).strokeDashArray([1,2])}build() {Stack({alignContent: Alignment.Center}) {MusicPlayer({url:'http://192.168.31.72/resource/audio/3142adb1eeda4e3fa3d4a26050d176c1.mp3'}).width("90%").height(50)}.width("100%").height("100%")}
}