尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

微信小程序音乐播放器完整实现:BackgroundAudioManager后台播放与进度同步

微信小程序音乐播放器完整实现:BackgroundAudioManager后台播放与进度同步 简介一套面向微信小程序开发者的音乐播放器完整代码适合初步接触小程序框架、希望借助真实项目掌握页面布局与音频播放逻辑的学习者。项目按标签栏区域、内容区域和播放器区域组织实现了专辑封面随音乐旋转、暂停时停止旋转、滑动选择器控制播放进度等典型交互目录结构清晰便于直接运行和二次修改。资源包共763个文件压缩后约5.05MB主要包含208个js逻辑文件、128个json配置、7个wxml和4个wxss页面文件以及mp3音频素材和依赖管理相关文件类型覆盖了小程序开发的主要环节。已有700人学习下载。借助该示例能够直观理解微信小程序音乐播放器的页面结构、组件调用与基础样式处理适合用于课程设计、毕业设计或功能模块参考。1. 微信小程序音乐播放器的「完整代码」难在哪微信小程序音乐播放器看着不复杂一个封面、一条进度条、三个按钮。可一套「完整代码」推到能上线卡点几乎不在 UI而在音频上下文、后台播放配置和进度同步。小程序没有 audio 标签播放音频只有 InnerAudioContext 和 BackgroundAudioManager 两条路选错后面全部返工。下面这套实现以 BackgroundAudioManager 为主线覆盖播放器单例、播放页 WXML、进度条双向同步以及真机后台播放配置与排错。适合做微信小程序毕业设计、课程设计的开发者也适合做电台、有声书类小程序的工程师。先说选择只在页面内放十几秒提示音用 InnerAudioContext要锁屏封面、切后台继续响、切歌可控必须走 BackgroundAudioManager并在 app.json 声明 requiredBackgroundModes。2. 选型与配置InnerAudioContext 和 BackgroundAudioManager 怎么选2.1 两条音频路线的 6 项对比InnerAudioContext 是页面级音频上下文每次通过wx.createInnerAudioContext()创建BackgroundAudioManager 是全局唯一的背景播放器wx.getBackgroundAudioManager()直接拿单例。两者 API 名字很像生命周期完全是两套逻辑对比项InnerAudioContextBackgroundAudioManager创建方式每次 createInnerAudioContext()getBackgroundAudioManager() 全局单例页面卸载后不自动销毁但 iOS 切后台即断常驻跟随系统播放会话后台播放不支持锁屏、不保证续播支持需声明 requiredBackgroundModes锁屏封面/标题无title、singer、coverImgUrl 可显示进度回调onTimeUpdateonTimeUpdate适合场景提示音、短音效、试听片段音乐、电台、有声书等长音频我一般把短音效单独用 InnerAudioContext避免和背景播放抢状态凡是名字里带「播放器」的长音频场景直接 BackgroundAudioManager。很多毕业设计项目用 InnerAudioContext 是因为页面代码写起来短但一旦演示到「切后台音乐还在响」就露馅了。2.2 app.json 声明 requiredBackgroundModes 的写法{ pages: [pages/index/index, pages/player/player], requiredBackgroundModes: [audio], window: { navigationBarTitleText: 音乐播放器 } }requiredBackgroundModes 只放在 app.json 顶层值是数组常用只有 audio 和 location。做音乐播放器只填 audio如果为了调试顺手填了 location提审时会被要求说明定位用途审核成本白白增加。这个配置有两条生效规则。第一在微信开发者工具里改完要重新编译并真机预览模拟器不表现后台播放行为第二Android 上不声明也可能后台续播但 iOS 不声明锁屏界面和后台播放一定拿不到。所以上线前务必用 iPhone 做最终验收。2.3 音频源的三条硬性约束第一音频地址必须是 HTTPS微信会拦截非安全域名的媒体请求第二格式优先 MP3/AACAndroid 机型对 WAV、FLAC 的兼容性参差建议服务端统一转 128kbps MP3第三线上环境需要在小程序后台把音频域名加进 downloadFile 合法域名开发阶段可以在开发者工具里勾选「不校验合法域名」但提审前必须补齐。本地文件不是不行包内路径可以直接传给 src但主包 2 MB 的限制下塞三首歌就超了。常见做法是把音频放云存储或 OSS用正式域名转发调试阶段用临时链接可以跑通逻辑但临时链接会过期不能写死在代码里。3. 完整代码落地播放器单例、播放页骨架与进度同步3.1 utils/player.js把背景播放器封装成全局单例BackgroundAudioManager 本身是全局的但直接散在页面里调用切歌逻辑和页面状态会越写越乱。干净的做法是单独建 utils/player.js把播放列表、当前索引、播放/暂停/上下曲收敛成一个单例模块// utils/player.js const bgm wx.getBackgroundAudioManager() const player { list: [], currentIndex: -1, setPlaylist(list) { this.list list }, play(index) { const song this.list[index] if (!song) return // 先赋值元数据再赋值 srciOS 锁屏才能显示封面和标题 bgm.title song.title bgm.singer song.singer bgm.epname song.album || bgm.coverImgUrl song.cover bgm.src song.url bgm.obeyMuteSwitch false // iOS 静音键不打断播放 this.currentIndex index }, toggle() { if (this.currentIndex 0) { this.play(0) return } if (bgm.paused) { bgm.play() } else { bgm.pause() } }, next() { const i (this.currentIndex 1) % this.list.length this.play(i) }, prev() { const i (this.currentIndex - 1 this.list.length) % this.list.length this.play(i) }, getManager() { return bgm } } module.exports player这段代码有四个关键点。其一player 对象通过 module.exports 导出require 多次拿到的还是同一份引用不会出现两个页面各持有一个播放器实例。其二title、singer、coverImgUrl 必须在 src 之前赋值否则 iOS 锁屏首次建立播放会话时拿不到封面。其三obeyMuteSwitch 只对 iOS 生效设成 false 后手机静音键不会把音乐一起关掉Android 上没有这个属性赋值不报错。其四每次重新赋值 src 就会触发加载和播放不需要手动调 bgm.play()但 pause 之后恢复需要调 play()。player 模块对外的方法很薄方便页面调用和测试方法作用注意事项setPlaylist(list)注入歌曲列表每项含 title/singer/cover/srcplay(index)播放指定索引元数据在前src 在后toggle()播放/暂停切换首次调用走 play(0)next() / prev()上下曲用取模保证边界不越界getManager()取底层播放器页面监听事件用3.2 播放页 WXML列表、封面、控制栏的最小骨架!-- pages/player/player.wxml -- view classplayer-page image classcover src{{currentSong.cover}} modeaspectFill / view classinfo-row text classsong-title{{currentSong.title}}/text text classsong-singer{{currentSong.singer}}/text /view !-- 进度条 max 用秒拖动用 bindchanging -- view classprogress-row text classtime{{formatTime(currentTime)}}/text slider value{{currentTime}} max{{duration}} step1 bindchangingonSliding bindchangeonSeek / text classtime{{formatTime(duration)}}/text /view view classbtn-row button sizemini bindtaponPrev上一首/button button sizemini bindtaponToggle{{playing ? 暂停 : 播放}}/button button sizemini bindtaponNext下一首/button /view view classsong-list view wx:for{{songs}} wx:keyid classsong-item {{index currentIndex ? active : }} >.player-page { display: flex; flex-direction: column; height: 100vh; } .cover { width: 600rpx; height: 600rpx; border-radius: 24rpx; margin: 60rpx auto 40rpx; } .song-list { flex: 1; overflow-y: auto; } .song-item.active { color: #07c160; }3.3 进度条拖动与 onTimeUpdate 的冲突处理背景播放器的 onTimeUpdate 大约每 250ms 触发一次用户正在拖 slider 时两边同时 setData 会把进度条拽回去。常见做法是加一个 sliding 标记Page({ data: { currentTime: 0, duration: 1, sliding: false, playing: false, currentSong: {} }, onLoad() { const player require(../../utils/player.js) this.player player player.setPlaylist(this.data.songs) player.play(0) const bgm player.getManager() bgm.onTimeUpdate(() { if (this.data.sliding) return this.setData({ currentTime: bgm.currentTime, duration: bgm.duration || this.data.duration }) }) bgm.onEnded(() player.next()) }, onSliding() { this.setData({ sliding: true }) }, onSeek(e) { const bgm this.player.getManager() bgm.seek(e.detail.value) this.setData({ currentTime: e.detail.value, sliding: false }) } })原理是 bindchanging 在拖动过程中连续触发只负责把 sliding 置为 truebindchange 在松手那一刻触发一次e.detail.value 是松手时的滑块位置。onSeek 里先调用 bgm.seek() 再重置 sliding顺序不能反否则 onTimeUpdate 可能在 seek 完成前先把旧的 currentTime 写回来。另外 onTimeUpdate 里 setData 的频率是每 250ms 一次数据量小问题不大如果一帧要同时更新封面、歌词、进度三个字段建议只在秒数发生变化时 setData降低渲染压力。3.4 生命周期onShow 同步状态、onUnload 不销毁音乐播放器页面的特殊之处在于播放器是全局的页面是临时的。所以 onShow 时必须把页面数据对齐到播放器真实状态onShow() { const bgm this.player.getManager() const song this.player.list[this.player.currentIndex] || {} this.setData({ currentSong: song, currentTime: bgm.currentTime || 0, duration: bgm.duration || 0, playing: !bgm.paused }) }, onUnload() { // 只做页面清理不去动 bgm.stop() }onUnload 里最忌讳调用 bgm.stop()很多初学者把「页面销毁」和「播放结束」混在一起结果从播放页跳回列表页音乐就断了。BackgroundAudioManager 没有 destroy 方法它是小程序全局唯一的播放句柄stop() 之后重新赋 src 也能恢复播放但「退出页面即停播」会直接毁掉后台播放这个核心能力。4. 后台播放实战锁屏封面、onEnded 状态机与真机抓包4.1 锁屏封面的顺序坑先元数据后 src第 3 章的代码里已经写了这个顺序这里单独强调为什么。iOS 锁屏展示的媒体信息来自系统播放会话会话在 bgm.src 赋值那一刻建立如果那时 title 和 coverImgUrl 还是空的系统就把空信息缓存下来之后补赋值也不会刷新锁屏只能等下一次 src 变更。bgm.title song.title bgm.singer song.singer bgm.epname song.album bgm.coverImgUrl song.cover bgm.src song.url封面图建议 500×500 以上、比例 1:1iOS 锁屏对非正方形封面会裁边。singer 和 epname 在 Android 上不一定显示但不赋值会导致部分机型锁屏切歌后标题错位所以每次 play() 都把这几个字段补齐。4.2 onEnded 的自我切换与列表播完策略onEnded 只在自然播放结束时触发手动切歌不会走到它。因此循环逻辑要自己控制bgm.onEnded(() { if (player.currentIndex player.list.length - 1) { bgm.stop() // 列表播完停在最后一首 return } player.next() })这里注意 onEnded 回调里不要再调同一首歌的 play(index)src 相同不会触发重新加载播放器会停在暂停态。循环播放的常规做法是给 player 加一个 loop 配置loop 为 true 时 onEnded 里走 player.play(0)为 false 时播完即停。切歌按钮不要依赖 onEnded它语义是「自然结束」手动上一首/下一首直接调 player.next() 即可。4.3 开发者工具、Android、iOS 三端的行为差异后台播放是典型的「模拟器一切正常、真机各种不对」模块三端差异值得单独记一份环境后台播放锁屏信息调试价值微信开发者工具无法模拟无Network 面板看请求、返回码Android 真机未声明也可能续播偶发丢失验证音频格式兼容性iOS 真机依赖 app.json 声明完整显示作为最终验收标准真机排查音频请求时打开微信开发者工具的「真机调试」Network 面板直接看音频请求返回码200 和 206 都算正常206 表示分段加载404/403 基本就是域名没配或防盗链拦截。单独录包时用 Charles 抓包也能看到同样信息注意区分音频 URL 和页面 JSON 请求音频流会持续请求分片看起来像「刷屏」是正常现象。iOS 上还要确认一次按 Home 切后台后锁屏有没有出现媒体卡片这一步开发者工具永远模拟不了。5. 上线前用一段 probe 脚本验证音频链路5.1 三行核心逻辑的播放预检音乐播放器上线后大部分问题出在音频源本身而不是小程序代码。为了不在提审后暴露习惯做法是在调试页挂一个「音频预检」按钮逻辑只有一段function probeAudio(url, onOk, onFail) { const bgm wx.getBackgroundAudioManager() bgm.title probe bgm.singer probe bgm.src url bgm.onCanplay(() { bgm.stop() onOk() }) bgm.onError((err) onFail(err)) }onCanplay 触发说明整条链路已经打通onError 里的 errCode 常见就三类域名未配置、URL 非 HTTPS、音频格式解析失败。注意 probe 会打断当前正在播放的音乐验收环境单独开一个小程序页面不要和正式播放页共用入口。5.2 提审前自查清单检查项方法判定标准app.json 声明搜索 requiredBackgroundModes包含 audio音频域名小程序后台 downloadFile 合法域名已加入且证书有效元数据顺序play() 里 title/cover 在 src 前顺序正确iOS 静音键真机拨静音键后播放声音不中断切后台播放中按 Home 键锁屏出现媒体卡片列表播完播到最后一首等待按预期停止或循环把这张清单和 probe 脚本一起埋进开发版正式版不做入口。真机上点 probe 看到 onCanplay 再提审万一走到 onError把 errMsg 截图给后端按清单里的域名、格式、HTTPS 证书三个方向排查定位时间能从小时级压到分钟级。本文还有配套的精品资源点击获取
返回列表