393 lines
13 KiB
JavaScript
Raw Normal View History

2025-06-06 15:17:30 +08:00
const utils = require("../../../utils/utils");
Page({
data: {
2026-08-04 09:59:35 +08:00
title: "音乐",
data: {},
playlist: [],
currentIndex: 0,
value: "00:00",
duration: "00:00",
durationSeconds: 1,
sliderValue: 0,
2026-08-04 09:59:35 +08:00
percent: 0,
isPlay: false,
isSingleLoop: false,
isShow: false,
isClosing: false
2025-06-06 15:17:30 +08:00
},
onLoad(options) {
2026-08-04 09:59:35 +08:00
let current = {};
try {
current = JSON.parse(decodeURIComponent(options.data || "{}"));
} catch (error) {
console.error("资源参数解析失败", error);
}
2025-06-06 15:17:30 +08:00
this.setData({
2026-08-04 09:59:35 +08:00
data: current,
playlist: current.resourceUrl ? [current] : [],
title: current.id == 1 ? "口令" : "音乐"
});
this.createAudioContext();
if (current.resourceUrl) {
this.loadTrack(current, false);
}
2025-06-06 15:17:30 +08:00
2026-08-04 09:59:35 +08:00
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]);
});
2026-08-04 09:59:35 +08:00
});
}
2025-06-06 15:17:30 +08:00
},
2026-08-04 09:59:35 +08:00
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);
2025-06-06 15:17:30 +08:00
},
2026-08-04 09:59:35 +08:00
loadTrack(track, autoplay = true) {
if (!track || !track.resourceUrl || !this.audioCtx) return;
this.cancelPendingDownload();
this.clearWaitingTimer();
const cachedDuration = Number(this.audioDurationCache[track.resourceUrl]) || 0;
2025-06-06 15:17:30 +08:00
this.setData({
2026-08-04 09:59:35 +08:00
data: track,
value: "00:00",
duration: cachedDuration ? utils.formatTimeStr(cachedDuration) : "00:00",
durationSeconds: cachedDuration || 1,
sliderValue: 0,
2026-08-04 09:59:35 +08:00
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();
}
}
2026-08-04 09:59:35 +08:00
});
},
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();
2025-06-06 15:17:30 +08:00
},
2026-08-04 09:59:35 +08:00
updateProgress() {
if (!this.audioCtx || this.isSeeking) return;
2026-08-04 09:59:35 +08:00
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;
2025-06-06 15:17:30 +08:00
this.setData({
2026-08-04 09:59:35 +08:00
value: utils.formatTimeStr(currentTime),
duration: duration ? utils.formatTimeStr(duration) : this.data.duration,
durationSeconds: duration || 1,
sliderValue: Math.min(currentTime, duration || 1),
2026-08-04 09:59:35 +08:00
percent: duration ? Math.min((currentTime / duration) * 100, 100) : 0
});
2025-06-06 15:17:30 +08:00
},
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;
},
2026-08-04 09:59:35 +08:00
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();
}
2025-06-06 15:17:30 +08:00
},
2026-08-04 09:59:35 +08:00
playPrevious() {
this.changeTrack(-1, true);
2025-06-06 15:17:30 +08:00
},
2026-08-04 09:59:35 +08:00
playNext() {
this.changeTrack(1, true);
},
2025-06-06 15:17:30 +08:00
2026-08-04 09:59:35 +08:00
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));
2025-06-06 15:17:30 +08:00
},
2026-08-04 09:59:35 +08:00
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();
});
2025-06-06 15:17:30 +08:00
},
2026-08-04 09:59:35 +08:00
toggleLoop() {
const isSingleLoop = !this.data.isSingleLoop;
this.setData({ isSingleLoop });
wx.showToast({ title: isSingleLoop ? "单曲循环" : "列表循环", icon: "none" });
},
2025-06-06 15:17:30 +08:00
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);
2025-06-06 15:17:30 +08:00
},
2026-08-04 09:59:35 +08:00
showList() {
if (this.closeTimer) {
clearTimeout(this.closeTimer);
this.closeTimer = null;
}
this.setData({ isShow: true, isClosing: false });
},
2025-06-06 15:17:30 +08:00
2026-08-04 09:59:35 +08:00
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);
2025-06-06 15:17:30 +08:00
},
2026-08-04 09:59:35 +08:00
preventClose() {},
2025-06-06 15:17:30 +08:00
2026-08-04 09:59:35 +08:00
onUnload() {
if (this.closeTimer) clearTimeout(this.closeTimer);
if (this.seekTimer) clearTimeout(this.seekTimer);
this.clearWaitingTimer();
this.cancelPendingDownload();
this.destroyDurationProbe();
2026-08-04 09:59:35 +08:00
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);
2026-08-04 09:59:35 +08:00
this.audioCtx = null;
}
2025-06-06 15:17:30 +08:00
}
2026-08-04 09:59:35 +08:00
});