const utils = require("../../../utils/utils"); Page({ data: { title: "音乐", data: {}, playlist: [], currentIndex: 0, value: "00:00", duration: "00:00", durationSeconds: 1, sliderValue: 0, percent: 0, isPlay: false, isSingleLoop: false, isShow: false, isClosing: false }, onLoad(options) { let current = {}; try { current = JSON.parse(decodeURIComponent(options.data || "{}")); } catch (error) { console.error("资源参数解析失败", error); } this.setData({ data: current, playlist: current.resourceUrl ? [current] : [], title: current.id == 1 ? "口令" : "音乐" }); this.createAudioContext(); if (current.resourceUrl) { this.loadTrack(current, false); } const eventChannel = this.getOpenerEventChannel && this.getOpenerEventChannel(); if (eventChannel && eventChannel.on) { eventChannel.on("setPlaylist", ({ playlist = [], currentIndex = 0 }) => { if (!playlist.length) return; const safeIndex = Math.min(Math.max(Number(currentIndex) || 0, 0), playlist.length - 1); this.setData({ playlist, currentIndex: safeIndex, data: playlist[safeIndex] }, () => { this.probeAudioDuration(playlist[safeIndex]); }); }); } }, createAudioContext() { this.audioCtx = wx.getBackgroundAudioManager(); this.audioCache = {}; this.audioDurationCache = {}; this.recoveryAttempted = {}; this.loadedTrackUrl = ""; this.audioHandlers = { canplay: () => this.updateProgress(), timeUpdate: () => { this.clearWaitingTimer(); this.updateProgress(); }, waiting: () => this.handleAudioWaiting(), play: () => this.setData({ isPlay: true }), pause: () => this.setData({ isPlay: false }), stop: () => this.setData({ isPlay: false }), ended: () => { if (this.data.isSingleLoop) { this.audioCtx.seek(0); this.audioCtx.play(); } else { this.changeTrack(1, true); } }, error: (error) => this.handleAudioError(error), prev: () => this.playPrevious(), next: () => this.playNext() }; this.audioCtx.onCanplay(this.audioHandlers.canplay); this.audioCtx.onTimeUpdate(this.audioHandlers.timeUpdate); this.audioCtx.onWaiting(this.audioHandlers.waiting); this.audioCtx.onPlay(this.audioHandlers.play); this.audioCtx.onPause(this.audioHandlers.pause); this.audioCtx.onStop(this.audioHandlers.stop); this.audioCtx.onEnded(this.audioHandlers.ended); this.audioCtx.onError(this.audioHandlers.error); this.audioCtx.onPrev(this.audioHandlers.prev); this.audioCtx.onNext(this.audioHandlers.next); }, loadTrack(track, autoplay = true) { if (!track || !track.resourceUrl || !this.audioCtx) return; this.cancelPendingDownload(); this.clearWaitingTimer(); const cachedDuration = Number(this.audioDurationCache[track.resourceUrl]) || 0; this.setData({ data: track, value: "00:00", duration: cachedDuration ? utils.formatTimeStr(cachedDuration) : "00:00", durationSeconds: cachedDuration || 1, sliderValue: 0, percent: 0, isPlay: false }); if (!cachedDuration) this.probeAudioDuration(track); if (!autoplay) return; const playableUrl = this.audioCache[track.resourceUrl] || track.resourceUrl; this.playAudioSource(track, playableUrl, 0); }, playAudioSource(track, playableUrl, startTime = 0) { if (!this.audioCtx || !track || !playableUrl) return; this.audioCtx.title = track.resourceName || this.data.title || "音频资源"; this.audioCtx.epname = this.data.title || "教学资源"; this.audioCtx.singer = "跃动"; this.audioCtx.startTime = Math.max(Number(startTime) || 0, 0); this.loadedTrackUrl = track.resourceUrl; this.audioCtx.src = playableUrl; }, handleAudioWaiting() { if (this.waitingTimer || this.downloadTask || !this.data.data.resourceUrl) return; const trackUrl = this.data.data.resourceUrl; const resumeTime = Number(this.audioCtx.currentTime) || 0; this.waitingTimer = setTimeout(() => { this.waitingTimer = null; if (this.data.data.resourceUrl === trackUrl && !this.audioCache[trackUrl]) { this.downloadAndPlay(this.data.data, Number(this.audioCtx.currentTime) || resumeTime); } }, 5000); }, handleAudioError(error = {}) { console.error("音频播放失败", { errCode: error.errCode, errMsg: error.errMsg, url: this.data.data.resourceUrl }); this.clearWaitingTimer(); this.setData({ isPlay: false }); const track = this.data.data; const trackUrl = track && track.resourceUrl; if (this.downloadTask) return; if (trackUrl && !this.audioCache[trackUrl] && !this.recoveryAttempted[trackUrl]) { this.downloadAndPlay(track, Number(this.audioCtx.currentTime) || 0); return; } const message = Number(error.errCode) === 10003 ? "音频文件响应异常,请检查文件配置" : "音频加载失败,请稍后重试"; wx.showToast({ title: message, icon: "none" }); }, downloadAndPlay(track, resumeTime = 0) { if (!track || !track.resourceUrl || this.downloadTask) return; const originalUrl = track.resourceUrl; this.recoveryAttempted[originalUrl] = true; const downloadState = { canceled: false, url: originalUrl }; this.downloadState = downloadState; wx.showLoading({ title: "音频加载中", mask: false }); this.downloadTask = wx.downloadFile({ url: originalUrl, success: (result) => { if (downloadState.canceled) return; if (result.statusCode !== 200 || !result.tempFilePath) { this.showDownloadError(); return; } this.audioCache[originalUrl] = result.tempFilePath; if (this.data.data.resourceUrl === originalUrl) { this.playAudioSource(track, result.tempFilePath, resumeTime); } }, fail: (error) => { if (downloadState.canceled) return; console.error("音频下载失败", error); this.showDownloadError(); }, complete: () => { if (this.downloadState === downloadState) { this.downloadTask = null; this.downloadState = null; wx.hideLoading(); } } }); }, showDownloadError() { this.setData({ isPlay: false }); wx.showToast({ title: "长音频加载失败,请检查网络或文件配置", icon: "none" }); }, clearWaitingTimer() { if (!this.waitingTimer) return; clearTimeout(this.waitingTimer); this.waitingTimer = null; }, cancelPendingDownload() { if (!this.downloadTask) return; if (this.downloadState) { this.downloadState.canceled = true; delete this.recoveryAttempted[this.downloadState.url]; } this.downloadTask.abort(); this.downloadTask = null; this.downloadState = null; wx.hideLoading(); }, updateProgress() { if (!this.audioCtx || this.isSeeking) return; const currentTime = Number(this.audioCtx.currentTime) || 0; const audioDuration = Number(this.audioCtx.duration) || 0; const knownDuration = this.data.durationSeconds > 1 ? this.data.durationSeconds : 0; const duration = audioDuration || knownDuration; this.setData({ value: utils.formatTimeStr(currentTime), duration: duration ? utils.formatTimeStr(duration) : this.data.duration, durationSeconds: duration || 1, sliderValue: Math.min(currentTime, duration || 1), percent: duration ? Math.min((currentTime / duration) * 100, 100) : 0 }); }, probeAudioDuration(track) { if (!track || !track.resourceUrl || !wx.createInnerAudioContext) return; this.destroyDurationProbe(); const trackUrl = track.resourceUrl; const probe = wx.createInnerAudioContext(); this.durationProbe = probe; probe.autoplay = false; probe.obeyMuteSwitch = false; const syncDuration = () => { if (this.durationProbe !== probe || this.data.data.resourceUrl !== trackUrl) return false; const duration = Number(probe.duration) || 0; if (duration <= 0) return false; this.audioDurationCache[trackUrl] = duration; this.setData({ duration: utils.formatTimeStr(duration), durationSeconds: duration }); this.destroyDurationProbe(); return true; }; probe.onCanplay(() => { if (syncDuration()) return; [200, 500, 1000, 2000].forEach((delay) => { const timer = setTimeout(syncDuration, delay); this.durationProbeTimers.push(timer); }); }); probe.onError((error) => { console.warn("音频时长读取失败", error); this.destroyDurationProbe(); }); this.durationProbeTimers = []; probe.src = trackUrl; }, destroyDurationProbe() { (this.durationProbeTimers || []).forEach((timer) => clearTimeout(timer)); this.durationProbeTimers = []; if (!this.durationProbe) return; this.durationProbe.destroy(); this.durationProbe = null; }, btnCilck() { if (!this.audioCtx || !this.data.data.resourceUrl) return; if (this.data.isPlay) { this.audioCtx.pause(); } else if (this.loadedTrackUrl !== this.data.data.resourceUrl) { this.loadTrack(this.data.data, true); } else { this.audioCtx.play(); } }, playPrevious() { this.changeTrack(-1, true); }, playNext() { this.changeTrack(1, true); }, changeTrack(step, autoplay) { const { playlist, currentIndex } = this.data; if (!playlist.length) return; const nextIndex = (currentIndex + step + playlist.length) % playlist.length; this.setData({ currentIndex: nextIndex }, () => this.loadTrack(playlist[nextIndex], autoplay)); }, selectTrack(e) { const index = Number(e.currentTarget.dataset.index); if (!Number.isInteger(index) || !this.data.playlist[index]) return; this.setData({ currentIndex: index }, () => { this.loadTrack(this.data.playlist[index], true); this.hideList(); }); }, toggleLoop() { const isSingleLoop = !this.data.isSingleLoop; this.setData({ isSingleLoop }); wx.showToast({ title: isSingleLoop ? "单曲循环" : "列表循环", icon: "none" }); }, onSliderChanging(e) { const seconds = Number(e.detail.value) || 0; this.isSeeking = true; this.setData({ sliderValue: seconds, value: utils.formatTimeStr(seconds), percent: this.data.durationSeconds ? (seconds / this.data.durationSeconds) * 100 : 0 }); }, onSliderChange(e) { const seconds = Number(e.detail.value) || 0; if (!this.audioCtx || !this.data.data.resourceUrl) return; if (this.loadedTrackUrl !== this.data.data.resourceUrl) { this.loadTrack(this.data.data, true); } this.audioCtx.seek(seconds); this.setData({ sliderValue: seconds, value: utils.formatTimeStr(seconds), percent: this.data.durationSeconds ? (seconds / this.data.durationSeconds) * 100 : 0 }); if (this.seekTimer) clearTimeout(this.seekTimer); this.seekTimer = setTimeout(() => { this.isSeeking = false; this.updateProgress(); this.seekTimer = null; }, 350); }, showList() { if (this.closeTimer) { clearTimeout(this.closeTimer); this.closeTimer = null; } this.setData({ isShow: true, isClosing: false }); }, hideList() { if (!this.data.isShow || this.data.isClosing) return; this.setData({ isClosing: true }); this.closeTimer = setTimeout(() => { this.setData({ isShow: false, isClosing: false }); this.closeTimer = null; }, 240); }, preventClose() {}, onUnload() { if (this.closeTimer) clearTimeout(this.closeTimer); if (this.seekTimer) clearTimeout(this.seekTimer); this.clearWaitingTimer(); this.cancelPendingDownload(); this.destroyDurationProbe(); if (this.audioCtx) { this.audioCtx.stop(); const handlers = this.audioHandlers || {}; if (this.audioCtx.offCanplay) this.audioCtx.offCanplay(handlers.canplay); if (this.audioCtx.offTimeUpdate) this.audioCtx.offTimeUpdate(handlers.timeUpdate); if (this.audioCtx.offWaiting) this.audioCtx.offWaiting(handlers.waiting); if (this.audioCtx.offPlay) this.audioCtx.offPlay(handlers.play); if (this.audioCtx.offPause) this.audioCtx.offPause(handlers.pause); if (this.audioCtx.offStop) this.audioCtx.offStop(handlers.stop); if (this.audioCtx.offEnded) this.audioCtx.offEnded(handlers.ended); if (this.audioCtx.offError) this.audioCtx.offError(handlers.error); if (this.audioCtx.offPrev) this.audioCtx.offPrev(handlers.prev); if (this.audioCtx.offNext) this.audioCtx.offNext(handlers.next); this.audioCtx = null; } } });