Uni-App 开发本地音乐播放器 Android 软件
前言
- 使用 Uni-App 开发本地音乐播放器 Android 软件,以下以 Tune 软件为示例。
页面静态开发
- 技术栈:
Vue3.x+Vite+TypeScript+Uni-App+Less
扫描音频文件
- Android 10+ 以上版本支持扫描音频文件
ts
/**
* 扫描 Android 目录
* latest
*/
export const useScanAndroidDirs = async (): Promise<SongItemType[]> => {
try {
const mediaStore = plus.android.importClass('android.provider.MediaStore') as unknown as MediaStoreType
const mainActivity = plus.android.runtimeMainActivity() as unknown as {
getContentResolver: () => PlusAndroidInstanceObject
}
const contentResolver = mainActivity.getContentResolver() // 这返回 Java 对象代理
// MediaStore URI
const audioUri = mediaStore.Audio.Media.EXTERNAL_CONTENT_URI
// 投影列(要查询的字段)
const projection = [
mediaStore.Audio.Media._ID, // 音频 ID
mediaStore.Audio.Media.DISPLAY_NAME, // 显示名称(文件名)
mediaStore.Audio.Media.TITLE, // 标题(可空)
mediaStore.Audio.Media.ARTIST, // 艺术家(可空)
mediaStore.Audio.Media.ALBUM, // 专辑(可空)
mediaStore.Audio.Media.ALBUM_ID, // 专辑 ID(可空)
mediaStore.Audio.Media.DATA, // 音频文件路径
mediaStore.Audio.Media.DURATION, // 持续时间(毫秒)
mediaStore.Audio.Media.SIZE // 文件大小(字节)
]
// 可选:过滤只查询音乐(非铃声/通知音等),IS_MUSIC=1
const selection = `${mediaStore.Audio.Media.IS_MUSIC} = 1`
// 执行查询:用 plus.android.invoke 调用 Java 方法
const cursor = (
plus.android.invoke as (
obj: string | PlusAndroidClassObject | PlusAndroidInstanceObject,
name: string,
...args: unknown[]
) => any
)(contentResolver, 'query', audioUri, projection, selection, null, null)
const audioList: SongItemType[] = []
if (!cursor) return []
// 移动游标并读取(同样用 invoke 调用 Cursor 方法)
while (plus.android.invoke(cursor, 'moveToNext')) {
const id = plus.android.invoke(
cursor,
'getLong',
plus.android.invoke(cursor, 'getColumnIndexOrThrow', mediaStore.Audio.Media._ID)
)
const title = plus.android.invoke(
cursor,
'getString',
plus.android.invoke(cursor, 'getColumnIndexOrThrow', mediaStore.Audio.Media.TITLE)
)
const name = plus.android.invoke(
cursor,
'getString',
plus.android.invoke(cursor, 'getColumnIndexOrThrow', mediaStore.Audio.Media.DISPLAY_NAME)
)
const artist = plus.android.invoke(
cursor,
'getString',
plus.android.invoke(cursor, 'getColumnIndexOrThrow', mediaStore.Audio.Media.ARTIST)
)
const path = plus.android.invoke(
cursor,
'getString',
plus.android.invoke(cursor, 'getColumnIndexOrThrow', mediaStore.Audio.Media.DATA)
)
const duration = plus.android.invoke(
cursor,
'getLong',
plus.android.invoke(cursor, 'getColumnIndexOrThrow', mediaStore.Audio.Media.DURATION)
)
const album = plus.android.invoke(
cursor,
'getString',
plus.android.invoke(cursor, 'getColumnIndexOrThrow', mediaStore.Audio.Media.ALBUM)
)
audioList.push({
id: `${id}`,
name: title || name,
artist: artist && artist !== '<unknown>' ? artist : '未知',
url: `/static/cover/0${songStore.state.coverIndex + 1}/${useRandom(1, 10)}.png`, // '/static/default-cover.svg',
audio: `file://${path}`,
createdTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
duration: duration / 1000,
album: album || '未知'
})
}
// 关闭游标(重要,避免内存泄漏)
plus.android.invoke(cursor, 'close')
return audioList
} catch {
return []
}
}
/**
* 扫描 Android 目录
*/
export const useScanDir = (): Promise<SongItemType[]> => {
return new Promise(resolve => {
plus.android.requestPermissions(
['android.permission.READ_MEDIA_AUDIO'],
res => {
// 存在未授权权限
if (res.deniedPresent.length) {
uni.showModal({
title: '提示',
content: '需要存储权限才能继续,是否重新申请?',
success: res => {
if (res.cancel) return
useScanDir()
}
})
return
}
// 存在永久拒绝权限
if (res.deniedAlways.length) {
uni.showModal({
title: '提示',
content: '永久拒绝存储权限,无法继续,是否去设置?',
success: res => {
if (res.cancel) return
try {
const main = plus.android.runtimeMainActivity() as AndroidSetting.PlusAndroidInstanceObjectExtra
const Intent = plus.android.importClass(
'android.content.Intent'
) as AndroidSetting.PlusAndroidClassObjectExtra
const Settings = plus.android.importClass('android.provider.Settings') as AndroidSetting.Setting
const Uri = plus.android.importClass('android.net.Uri') as AndroidSetting.URI
const intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
const uri = Uri.fromParts('package', main.getPackageName(), null)
intent.setData(uri)
main.startActivity(intent)
} catch {
useToast('无法打开设置页')
}
}
})
return
}
useScanAndroidDirs().then(resolve)
},
() => {
uni.showModal({ title: '提示', content: '权限申请失败', showCancel: false })
}
)
})
}- 扫描获取歌曲列表
ts
/**
* 歌曲项类型
*/
interface SongItemType {
/**
* 歌曲 ID
*/
id: string
/**
* 歌曲名称
*/
name: string
/**
* 歌手
*/
artist: string
/**
* 封面 URL
*/
url: string
/**
* 音频 URL
*/
audio: string
/**
* 创建时间
*/
createdTime: string
/**
* 时长(秒)
*/
duration: number
/**
* 专辑
*/
album: string
}
const res: SongItemType[] = await useScanDir()播放歌曲
ts
/**
* 当前播放的音频 id
*/
let currentPlayId: string = ''
/**
* 音频播放
*/
export const useAudio = () => {
const playState = computed(() => songStore.state.isPlay)
let bgAudioManager: UniApp.BackgroundAudioManager | null = null
const initAudio = () => {
if (!songStore.playItem.value) return
if (bgAudioManager) {
// 避免重复初始化
if (currentPlayId === songStore.state.playId) return
bgAudioManager.stop()
} else {
bgAudioManager = uni.getBackgroundAudioManager()
}
currentPlayId = songStore.playItem.value.id
bgAudioManager.title = songStore.playItem.value.name
bgAudioManager.singer = songStore.playItem.value.artist
bgAudioManager.src = songStore.playItem.value.audio
bgAudioManager.coverImgUrl = songStore.playItem.value.url
// bgAudioManager.onPlay(() => {
// console.log('播放中')
// })
// bgAudioManager.onPause(() => {
// console.log('暂停')
// })
// bgAudioManager.onStop(() => {
// console.log('停止')
// })
bgAudioManager.onEnded(() => {
// console.log('播放结束')
bgAudioManager = null
songStore.onPlayPrevNext(1)
})
bgAudioManager.onTimeUpdate(() => {
if (!bgAudioManager) return
const currentTime = ~~bgAudioManager.currentTime
const duration = ~~bgAudioManager.duration
if (!duration) {
songStore.onTogglePlay(false)
useToast('播放失败')
return
}
songStore.onUpdateDuration({ currentTime, duration })
})
bgAudioManager.onError(err => {
if (!bgAudioManager) return
songStore.onTogglePlay(false)
// console.error('音频错误', err)
useToast('播放失败')
})
}
watch(
playState,
bool => {
if (!songStore.playItem.value) return
initAudio()
if (!bgAudioManager) return
if (bool) {
bgAudioManager.seek(songStore.state.currentTime)
bgAudioManager.play()
} else {
bgAudioManager.pause()
}
},
{ immediate: true }
)
}界面预览
