开源 Arkts 鸿蒙应用 开发(十三)音频--MP3播放
文章的目的为了记录使用Arkts 进行Harmony app 开发学习的经历。本职为嵌入式软件开发,公司安排开发app,临时学习,完成app的开发。开发流程和要点有些记忆模糊,赶紧记录,防止忘记。
相关链接:
开源 Arkts 鸿蒙应用 开发(一)工程文件分析-CSDN博客
开源 Arkts 鸿蒙应用 开发(二)封装库.har制作和应用-CSDN博客
开源 Arkts 鸿蒙应用 开发(三)Arkts的介绍-CSDN博客
开源 Arkts 鸿蒙应用 开发(四)布局和常用控件-CSDN博客
开源 Arkts 鸿蒙应用 开发(五)控件组成和复杂控件-CSDN博客
开源 Arkts 鸿蒙应用 开发(六)数据持久--文件和首选项存储-CSDN博客
开源 Arkts 鸿蒙应用 开发(七)数据持久--sqlite关系数据库-CSDN博客
开源 Arkts 鸿蒙应用 开发(八)多媒体--相册和相机-CSDN博客
开源 Arkts 鸿蒙应用 开发(九)通讯--tcp客户端-CSDN博客
开源 Arkts 鸿蒙应用 开发(十)通讯--Http-CSDN博客
开源 Arkts 鸿蒙应用 开发(十一)证书和包名修改-CSDN博客
开源 Arkts 鸿蒙应用 开发(十二)传感器的使用-CSDN博客
推荐链接:
开源 java android app 开发(一)开发环境的搭建-CSDN博客
开源 java android app 开发(二)工程文件结构-CSDN博客
开源 java android app 开发(三)GUI界面布局和常用组件-CSDN博客
开源 java android app 开发(四)GUI界面重要组件-CSDN博客
开源 java android app 开发(五)文件和数据库存储-CSDN博客
开源 java android app 开发(六)多媒体使用-CSDN博客
开源 java android app 开发(七)通讯之Tcp和Http-CSDN博客
开源 java android app 开发(八)通讯之Mqtt和Ble-CSDN博客
开源 java android app 开发(九)后台之线程和服务-CSDN博客
开源 java android app 开发(十)广播机制-CSDN博客
开源 java android app 开发(十一)调试、发布-CSDN博客
开源 java android app 开发(十二)封库.aar-CSDN博客
推荐链接:
开源C# .net mvc 开发(一)WEB搭建_c#部署web程序-CSDN博客
开源 C# .net mvc 开发(二)网站快速搭建_c#网站开发-CSDN博客
开源 C# .net mvc 开发(三)WEB内外网访问(VS发布、IIS配置网站、花生壳外网穿刺访问)_c# mvc 域名下不可訪問內網,內網下可以訪問域名-CSDN博客
开源 C# .net mvc 开发(四)工程结构、页面提交以及显示_c#工程结构-CSDN博客
开源 Arkts 鸿蒙应用 开发(十)通讯--Http数据传输-CSDN博客开源 C# .net mvc 开发(五)常用代码快速开发_c# mvc开发-CSDN博客
本章内容主HarmonyOS next 系统上的本机音频怎么播放,实现了一个简易的音乐播放器,可以播放App资源文件夹下的3首mp3。
1.代码结构分析
2.文件分析说明
3.显示效果
一、代码结构分析
需要添加和修改的文件如下:
entry/src/main/ets/pages 文件夹下需要有App.ets,Index.et,List.ets。
entry/src/main/resoures/base/profile/main_pages.json 页面配置文件也需要修改
entry/src/main/resoures/rawfile 文件加下需要有3个.mp3文件
二、文件分析说明
2.1 List.ets显示了音乐的列表,选择以后,跳转到Index.ets页面播放相应的音乐
以下为 List.ets代码
import { router } from '@kit.ArkUI';
import storage from './App';@Entry(storage)
@Component
struct Index {@LocalStorageLink('myData') myData: string = 'test_01';// 列表数据 - 字符串数组private dataList: string[] = ["test_01","test_02","test_03",];build() {Column() {// 列表标题Text('音乐列表').fontSize(24).fontWeight(FontWeight.Bold).margin({ top: 20, bottom: 10 })// ListView 实现List({ space: 10 }) {ForEach(this.dataList, (item: string) => {ListItem() {Text(item).fontSize(18).width('100%').height(60).textAlign(TextAlign.Center).backgroundColor(Color.White).borderRadius(8)}.onClick(() => {this.myData = item.toString();console.log("myData1",this.myData);// 点击跳转到详情页,传递当前项内容router.push({url: 'pages/Index',});})})}.width('100%').layoutWeight(1) // 占据剩余空间.margin(10).divider({strokeWidth: 1,color: '#EEEEEE',startMargin: 20,endMargin: 20})}.width('100%').height('100%').backgroundColor('#F5F5F5')}
}
2.2 Index.ets 文件,通过AVPlayer可以实现端到端播放原始媒体资源。
1)播放的全流程包含:创建AVPlayer,设置播放资源,设置播放参数(音量/倍速/焦点模式),播放控制(播放/暂停/跳转/停止),重置,销毁资源。
播放状态变化示意图
2)函数分析
AVPlayer封装
使用@ohos.multimedia.media模块的AVPlayer实现媒体播放
提供play()、pause()、seek()、setSpeed()等基本控制方法
通过fdSrc从资源文件加载媒体
状态管理:
监听stateChange事件处理播放器状态(idle/initialized/prepared/playing等)
播放器初始化
async avSetupAudio() {// 获取资源文件描述符let fileDescriptor = await this.context.resourceManager.getRawFd(this.fileName);let avFileDescriptor = { fd: fileDescriptor.fd, offset: fileDescriptor.offset, length: fileDescriptor.length };// 创建AVPlayer实例this.avPlayer = await media.createAVPlayer();// 设置回调await this.setAVPlayerCallback(...);// 设置播放源this.avPlayer.fdSrc = avFileDescriptor;
}
状态回调处理
this.avPlayer.on('stateChange', async (state, reason) => {switch (state) {case 'initialized':// 设置Surface并准备播放this.avPlayer.surfaceId = this.surfaceId;this.avPlayer.prepare();break;case 'prepared':// 开始播放并设置初始倍速this.avPlayer.play();break;case 'playing':// 更新UI状态break;// 其他状态处理...}
});
3)以下为Index.ets代码
/** Copyright (c) 2023-2025 Huawei Device Co., Ltd.* Licensed under the Apache License, Version 2.0 (the "License");* you may not use this file except in compliance with the License.* You may obtain a copy of the License at** http://www.apache.org/licenses/LICENSE-2.0** Unless required by applicable law or agreed to in writing, software* distributed under the License is distributed on an "AS IS" BASIS,* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.* See the License for the specific language governing permissions and* limitations under the License.*/import display from '@ohos.display';
import emitter from '@ohos.events.emitter';
import { common } from '@kit.AbilityKit';
import media from '@ohos.multimedia.media';
import { router } from '@kit.ArkUI';
import storage from './App';const PROPORTION = 0.99; // 占屏幕比例
const SURFACE_W = 0.9; // 表面宽比例
const SURFACE_H = 1.78; // 表面高比例
const SET_INTERVAL = 100; // interval间隔时间
const TIME_ONE = 60000;
const TIME_TWO = 1000;
const SPEED_ZERO = 0;
const SPEED_ONE = 1;
const SPEED_TWO = 2;
const SPEED_THREE = 3;
const SPEED_COUNT = 4;
let innerEventFalse: emitter.InnerEvent = {eventId: 1,priority: emitter.EventPriority.HIGH
};
let innerEventTrue: emitter.InnerEvent = {eventId: 2,priority: emitter.EventPriority.HIGH
};
let innerEventWH: emitter.InnerEvent = {eventId: 3,priority: emitter.EventPriority.HIGH
};@Entry(storage)
@Component
struct Index {@LocalStorageLink('myData') myData: string = '';tag: string = 'AVPlayManager';private xComponentController: XComponentController = new XComponentController();private avPlayer: media.AVPlayer | null = null;private surfaceId: string = '';private intervalID: number = -1;private seekTime: number = -1;private context: common.UIAbilityContext | undefined = undefined;private count: number = 0;@State fileName: string = 'test_01.mp3';@State isSwiping: boolean = false; // 用户滑动过程中@State isPaused: boolean = true; // 暂停播放@State XComponentFlag: boolean = false;@State speedSelect: number = 0; // 倍速选择@State speedList: Resource[] = [$r('app.string.video_speed_1_0X'), $r('app.string.video_speed_1_25X'), $r('app.string.video_speed_1_75X'), $r('app.string.video_speed_2_0X')];@StorageLink('durationTime') durationTime: number = 0; // 视频总时长@StorageLink('currentTime') currentTime: number = 0; // 视频当前时间@StorageLink('speedName') speedName: Resource = $r('app.string.video_speed_1_0X');@StorageLink('speedIndex') speedIndex: number = 0; // 倍速索引@State surfaceW: number | null = null;@State surfaceH: number | null = null;@State percent: number = 0;@State windowWidth: number = 300;@State windowHeight: number = 200;getDurationTime(): number {return this.durationTime;}getCurrentTime(): number {return this.currentTime;}timeConvert(time: number): string {let min: number = Math.floor(time / TIME_ONE);let second: string = ((time % TIME_ONE) / TIME_TWO).toFixed(0);// return `${min}:${(+second < TIME_THREE ? '0' : '') + second}`;second = second.padStart(2, '0');return `${min}:${second}`;}async msleepAsync(ms: number): Promise<boolean> {return new Promise((resolve, reject) => {setTimeout(() => {resolve(true)}, ms)})}async avSetupAudio() {// 通过UIAbilityContext的resourceManager成员的getRawFd接口获取媒体资源播放地址。// 返回类型为{fd,offset,length},fd为HAP包fd地址,offset为媒体资源偏移量,length为播放长度。if (this.context == undefined) return;let fileDescriptor = await this.context.resourceManager.getRawFd(this.fileName);let avFileDescriptor: media.AVFileDescriptor ={ fd: fileDescriptor.fd, offset: fileDescriptor.offset, length: fileDescriptor.length };if (this.avPlayer) {console.info(`${this.tag}: init avPlayer release2createNew`);this.avPlayer.release();await this.msleepAsync(1500);}// 创建avPlayer实例对象this.avPlayer = await media.createAVPlayer();// 创建状态机变化回调函数await this.setAVPlayerCallback((avPlayer: media.AVPlayer) => {this.percent = avPlayer.width / avPlayer.height;this.setVideoWH();this.durationTime = this.getDurationTime();setInterval(() => { // 更新当前时间if (!this.isSwiping) {this.currentTime = this.getCurrentTime();}}, SET_INTERVAL);});// 为fdSrc赋值触发initialized状态机上报this.avPlayer.fdSrc = avFileDescriptor;}avPlay(): void {if (this.avPlayer) {try {this.avPlayer.play();} catch (e) {console.error(`${this.tag}: avPlay = ${JSON.stringify(e)}`);}}}avPause(): void {if (this.avPlayer) {try {this.avPlayer.pause();console.info(`${this.tag}: avPause==`);} catch (e) {console.error(`${this.tag}: avPause== ${JSON.stringify(e)}`);}}}async avSeek(seekTime: number, mode: SliderChangeMode): Promise<void> {if (this.avPlayer) {try {console.info(`${this.tag}: videoSeek seekTime== ${seekTime}`);this.avPlayer.seek(seekTime, 2);this.currentTime = seekTime;} catch (e) {console.error(`${this.tag}: videoSeek== ${JSON.stringify(e)}`);}}}avSetSpeed(speed: number): void {if (this.avPlayer) {try {this.avPlayer.setSpeed(speed);console.info(`${this.tag}: avSetSpeed enum ${speed}`);} catch (e) {console.error(`${this.tag}: avSetSpeed == ${JSON.stringify(e)}`);}}}// 注册avplayer回调函数async setAVPlayerCallback(callback: (avPlayer: media.AVPlayer) => void, vType?: number): Promise<void> {// seek操作结果回调函数if (this.avPlayer == null) {console.error(`${this.tag}: avPlayer has not init!`);return;}this.avPlayer.on('seekDone', (seekDoneTime) => {console.info(`${this.tag}: setAVPlayerCallback AVPlayer seek succeeded, seek time is ${seekDoneTime}`);});this.avPlayer.on('speedDone', (speed) => {console.info(`${this.tag}: setAVPlayerCallback AVPlayer speedDone, speed is ${speed}`);});// error回调监听函数,当avPlayer在操作过程中出现错误时调用reset接口触发重置流程this.avPlayer.on('error', (err) => {console.error(`${this.tag}: setAVPlayerCallback Invoke avPlayer failed ${JSON.stringify(err)}`);if (this.avPlayer == null) {console.error(`${this.tag}: avPlayer has not init on error`);return;}this.avPlayer.reset();});// 状态机变化回调函数this.avPlayer.on('stateChange', async (state, reason) => {if (this.avPlayer == null) {console.info(`${this.tag}: avPlayer has not init on state change`);return;}switch (state) {case 'idle': // 成功调用reset接口后触发该状态机上报console.info(`${this.tag}: setAVPlayerCallback AVPlayer state idle called.`);break;case 'initialized': // avplayer 设置播放源后触发该状态上报console.info(`${this.tag}: setAVPlayerCallback AVPlayer state initialized called.`);if (this.surfaceId) {this.avPlayer.surfaceId = this.surfaceId; // 设置显示画面,当播放的资源为纯音频时无需设置console.info(`${this.tag}: setAVPlayerCallback this.avPlayer.surfaceId = ${this.avPlayer.surfaceId}`);this.avPlayer.prepare();}break;case 'prepared': // prepare调用成功后上报该状态机console.info(`${this.tag}: setAVPlayerCallback AVPlayer state prepared called.`);this.avPlayer.on('bufferingUpdate', (infoType: media.BufferingInfoType, value: number) => {console.info(`${this.tag}: bufferingUpdate called, infoType value: ${infoType}, value:${value}}`);})this.durationTime = this.avPlayer.duration;this.currentTime = this.avPlayer.currentTime;this.avPlayer.play(); // 调用播放接口开始播放console.info(`${this.tag}:setAVPlayerCallback speedSelect: ${this.speedSelect}, duration: ${this.durationTime}`);if (this.speedSelect != -1) {switch (this.speedSelect) {case SPEED_ZERO:this.avSetSpeed(media.PlaybackSpeed.SPEED_FORWARD_1_00_X);break;case SPEED_ONE:this.avSetSpeed(media.PlaybackSpeed.SPEED_FORWARD_1_25_X);break;case SPEED_TWO:this.avSetSpeed(media.PlaybackSpeed.SPEED_FORWARD_1_75_X);break;case SPEED_THREE:this.avSetSpeed(media.PlaybackSpeed.SPEED_FORWARD_2_00_X);break;}}callback(this.avPlayer);break;case 'playing': // play成功调用后触发该状态机上报console.info(`${this.tag}: setAVPlayerCallback AVPlayer state playing called.`);if (this.count !== 0) {if (this.intervalID != -1) {clearInterval(this.intervalID)}this.intervalID = setInterval(() => { // 更新当前时间AppStorage.setOrCreate('durationTime', this.durationTime);AppStorage.setOrCreate('currentTime', this.currentTime);}, 100);let eventDataTrue: emitter.EventData = {data: {'flag': true}};let innerEventTrue: emitter.InnerEvent = {eventId: 2,priority: emitter.EventPriority.HIGH};emitter.emit(innerEventTrue, eventDataTrue);} else {setTimeout(() => {console.info('AVPlayer playing wait to pause');this.avPlayer?.pause(); // 播放3s后调用暂停接口暂停播放。}, 3000);}this.count++;break;case 'completed': // 播放结束后触发该状态机上报console.info(`${this.tag}: setAVPlayerCallback AVPlayer state completed called.`);let eventDataFalse: emitter.EventData = {data: {'flag': false}};let innerEvent: emitter.InnerEvent = {eventId: 1,priority: emitter.EventPriority.HIGH};emitter.emit(innerEvent, eventDataFalse);if (this.intervalID != -1) {clearInterval(this.intervalID)}this.avPlayer.off('bufferingUpdate')AppStorage.setOrCreate('currentTime', this.durationTime);break;case 'released':console.info(`${this.tag}: setAVPlayerCallback released called.`);breakcase 'stopped':console.info(`${this.tag}: setAVPlayerCallback AVPlayer state stopped called.`);breakcase 'error':console.error(`${this.tag}: setAVPlayerCallback AVPlayer state error called.`);breakcase 'paused':console.info(`${this.tag}: setAVPlayerCallback AVPlayer state paused called.`);setTimeout(() => {console.info('AVPlayer paused wait to play again');this.avPlayer?.play(); // 暂停3s后再次调用播放接口开始播放。}, 3000);breakdefault:console.info(`${this.tag}: setAVPlayerCallback AVPlayer state unknown called.`);break;}});// 时间上报监听函数this.avPlayer.on('timeUpdate', (time: number) => {this.currentTime = time;});}aboutToAppear() {this.windowWidth = display.getDefaultDisplaySync().width;this.windowHeight = display.getDefaultDisplaySync().height;this.surfaceW = this.windowWidth * SURFACE_W;this.surfaceH = this.surfaceW / SURFACE_H;this.isPaused = true;this.context = getContext(this) as common.UIAbilityContext;}aboutToDisappear() {if (this.avPlayer == null) {console.info(`${this.tag}: avPlayer has not init aboutToDisappear`);return;}this.avPlayer.release((err) => {if (err == null) {console.info(`${this.tag}: videoRelease release success`);} else {console.error(`${this.tag}: videoRelease release failed, error message is = ${JSON.stringify(err.message)}`);}});emitter.off(innerEventFalse.eventId);}onPageHide() {this.avPause();this.isPaused = false;}onPageShow() {//const params = router.getParams();//this.fileName = params.toString();console.log("myData2",this.myData);this.fileName = this.myData+".mp3";console.log("myData2",this.fileName);emitter.on(innerEventTrue, (res: emitter.EventData) => {if (res.data) {this.isPaused = res.data.flag;this.XComponentFlag = res.data.flag;}});emitter.on(innerEventFalse, (res: emitter.EventData) => {if (res.data) {this.isPaused = res.data.flag;}});emitter.on(innerEventWH, (res: emitter.EventData) => {if (res.data) {this.windowWidth = res.data.width;this.windowHeight = res.data.height;this.setVideoWH();}});
}setVideoWH(): void {if (this.percent >= 1) { // 横向视频this.surfaceW = Math.round(this.windowWidth * PROPORTION);this.surfaceH = Math.round(this.surfaceW / this.percent);} else { // 纵向视频this.surfaceH = Math.round(this.windowHeight * PROPORTION);this.surfaceW = Math.round(this.surfaceH * this.percent);}
}@Builder
CoverXComponent() {XComponent({// 装载视频容器id: 'xComponent',type: XComponentType.SURFACE,controller: this.xComponentController}).id('VideoView').visibility(this.XComponentFlag ? Visibility.Visible : Visibility.Hidden).onLoad(() => {this.surfaceId = this.xComponentController.getXComponentSurfaceId();this.avSetupAudio();}).height(`${this.surfaceH}px`).width(`${this.surfaceW}px`)
}build() {Column() {Stack() {Column() {this.CoverXComponent()}.align(Alignment.TopStart).margin({ top: 80 }).id('VideoView').justifyContent(FlexAlign.Center)Row(){Image($r('app.media.vinyl')).width('100%').height('40%')}Text().height(`${this.surfaceH}px`).width(`${this.surfaceW}px`).margin({ top: 80 }).backgroundColor(Color.Black).opacity($r('app.float.size_zero_five')).visibility(this.isSwiping ? Visibility.Visible : Visibility.Hidden)Row() {Text(this.timeConvert(this.currentTime)).id("currentTime").fontSize($r('app.float.size_24')).opacity($r('app.float.size_1')).fontColor($r("app.color.slider_selected"))Text("/" + this.timeConvert(this.durationTime)).id("durationTime").fontSize($r('app.float.size_24')).opacity($r('app.float.size_1')).fontColor(Color.White)}.margin({ top: 80 }).visibility(this.isSwiping ? Visibility.Visible : Visibility.Hidden)Column() {Blank()Column() {// 进度条Row() {Row() {// 播放、暂停键Image(this.isPaused ? $r("app.media.ic_video_play") : $r("app.media.ic_video_pause"))// 暂停/播放.id(this.isPaused ? 'pause' : 'play').width($r('app.float.size_40')).height($r('app.float.size_40')).onClick(() => {if (this.isPaused) {this.avPause();this.isPaused = false;} else {this.avPlay();this.isPaused = true;}})// 左侧时间Text(this.timeConvert(this.currentTime)).id("currentTimeText").fontColor(Color.White).textAlign(TextAlign.End).fontWeight(FontWeight.Regular).margin({ left: $r('app.float.size_10') })}// 进度条Row() {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 (this.seekTime !== value) {this.seekTime = value;this.avSeek(Number.parseInt(value.toFixed(0)), mode);}})}.layoutWeight(1)Row() {// 右侧时间Text(this.timeConvert(this.durationTime)).id("durationTimeText").fontColor(Color.White).fontWeight(FontWeight.Regular)// 倍速按钮Button(this.speedName, { type: ButtonType.Normal }).border({ width: $r('app.float.size_1'), color: Color.White }).width(75).height($r('app.float.size_40')).fontSize($r('app.float.size_15')).borderRadius($r('app.float.size_24')).fontColor(Color.White).backgroundColor(Color.Black).opacity($r('app.float.size_1')).margin({ left: $r('app.float.size_10') }).id('Speed').onClick(() => {this.speedIndex = (this.speedIndex + 1) % SPEED_COUNT;this.speedSelect = this.speedIndex;this.speedName = this.speedList[this.speedIndex];if(!this.avPlayer) return;switch (this.speedSelect) {case 0:this.avSetSpeed(media.PlaybackSpeed.SPEED_FORWARD_1_00_X);break;case 1:this.avSetSpeed(media.PlaybackSpeed.SPEED_FORWARD_1_25_X);break;case 2:this.avSetSpeed(media.PlaybackSpeed.SPEED_FORWARD_1_75_X);break;case 3:this.avSetSpeed(media.PlaybackSpeed.SPEED_FORWARD_2_00_X);break;}})}}.justifyContent(FlexAlign.Center).padding({ left: $r('app.float.size_25'), right: $r('app.float.size_30') }).width('100%')}.width('100%').justifyContent(FlexAlign.Center)}.width('100%').height('100%')}.backgroundColor(Color.Black).height('90%').width('100%')Row() {Text(this.fileName).fontSize($r('app.float.size_20')).fontColor(Color.White).opacity($r('app.float.size_zero_six')).fontWeight(FontWeight.Regular).textAlign(TextAlign.Center)}Row() {Text("").fontSize($r('app.float.size_20')).fontColor(Color.White).opacity($r('app.float.size_zero_six')).fontWeight(FontWeight.Regular).textAlign(TextAlign.Center)}}.backgroundColor(Color.Black).height('100%').width('100%')
}
}
2.3 App.ets文件,通过LocalStorage实现组件间数据共享
// App.ets
const storage = new LocalStorage();
export default storage;
三、显示效果