class OptimizedWavPlayer {
constructor(filePath, meetingId, pageContext) {
this.filePath = filePath;
this.meetingId = meetingId;
this.pageContext = pageContext;
// 配置参数 – 优化超大文件播放
this.chunkSize = 1024 * 1024 * 1; // 调整为2MB,平衡流畅度和加载速度
this.preloadDistance = 3; // 增加预加载距离,提前缓存更多分片
this.retryCount = 3; // 重试次数
this.requestTimeout = 30000; // 超时时间
this.maxCachedChunks = 5; // 增加最大缓存分片数,减少重复下载
// 状态管理
this.audioContext = null;
this.totalSize = 0;
this.totalChunks = 0;
this.downloadedChunks = new Map(); // LRU缓存已下载的分片
this.isPlaying = false;
this.isInitialized = false;
this.loadingStatus = 'idle';
this.networkType = 'unknown';
this.localFilePath = '';
this.loadingTimer = null;
this.isAudioLoading = false; // 加载动画状态(与页面变量名保持一致)
this.failedChunkRanges = new Map();
this.minChunkSize = 1024 * 256; // 减小为256KB,更适应网络波动
this.currentPlayingChunk = -1;
this.playbackBufferSize = 1024 * 512; // 512KB播放缓冲区
this.lastPlayTime = 0;
this.isSwitchingChunk = false;
this.chunkOffsets = []; // 存储每个分片的起始位置
this.chunkSizes = []; // 存储每个分片的实际大小
this.initialized = false;
// 添加累计播放时间支持
this.accumulatedTime = 0; // 累计播放时间(秒)
this.lastSliceStartTime = 0; // 记录上一个切片的开始时间
this.progressInterval = null; // 进度更新定时器
this.duration = 0; // 切片累计时长
this.currentTime = 0; // wav文件总时长
// 初始化时确保页面显示00:00的时长
if (this.pageContext && typeof this.pageContext.setData === 'function') {
this.pageContext.setData({
duration: “00:00”,
progress: 0
});
}
this.initAudioContext();
this.detectNetworkType();
}
/**
* 初始化音频上下文 – 增强流式播放能力
*/
initAudioContext() {
this.audioContext = wx.createInnerAudioContext();
this.audioContext.autoplay = false;
this.audioContext.obeyMuteSwitch = false;
this.audioContext.buffer = true; // 启用缓冲,更适合流式播放
this.audioContext.onTimeUpdate(() => {
// 只在播放状态下更新进度,避免状态不一致
if (this.isPlaying) {
this.updatePlayProgress();
this.preloadNextChunks();
this.checkAndCleanOldChunks(); // 清理不再需要的分片
this.checkChunkSwitch(); // 确保分片切换正常触发
}
});
this.audioContext.onEnded(() => {
// 防止在切换过程中触发结束事件处理
if (this.isSwitchingChunk) return;
if (this.currentPlayingChunk >= this.totalChunks – 1) {
// 播放结束,重置累计时间变量
this.isPlaying = false;
this.accumulatedTime = 0;
this.lastSliceStartTime = 0;
// 确保播放结束时状态正确同步
try {
this.pageContext.setData({
isPlaying: false,
progress: 100,
currentTime: this.formatTime(this.duration)
});
console.log('播放结束,状态已更新为:false');
} catch (e) {
console.warn('更新页面状态失败:', e);
}
this.updateStatus('播放完成');
} else {
// 切换到下一个分片
this.switchToNextChunk(this.currentPlayingChunk + 1);
}
});
this.audioContext.onError((res) => {
// 防止在切换过程中触发错误事件处理
if (this.isSwitchingChunk) return;
const errorMsg = `播放错误[${res.errCode}]:${res.errMsg}`;
// 尝试从错误中恢复
if (res.errCode === 10001 && this.isPlaying) {
this.handlePlaybackError();
} else {
// 立即更新状态,避免状态不一致
this.isPlaying = false;
this.pageContext.setData({ isPlaying: false });
this.handleError(errorMsg);
}
});
this.audioContext.onWaiting(() => {
this.loadingTimer = setTimeout(() => {
this.updateStatus('音频缓冲中…');
// 不直接报错,尝试降级策略
this.adaptiveChunkSize();
}, 10000);
});
this.audioContext.onCanplay(() => {
if (this.loadingTimer) {
clearTimeout(this.loadingTimer);
this.loadingTimer = null;
}
this.updateStatus('音频可播放');
});
// 增强错误恢复能力
this.audioContext.onStop(() => {
// 只有在预期外的停止时才尝试恢复
if (this.loadingStatus === 'error' && this.isPlaying) {
this.handlePlaybackError();
}
});
}
/**
* 检测网络类型
*/
detectNetworkType() {
wx.getNetworkType({
success: (res) => {
this.networkType = res.networkType;
this.pageContext.setData({ networkType: res.networkType });
// 根据网络类型调整分片大小
if (['2g', '3g'].includes(this.networkType)) {
this.chunkSize = 1024 * 1024 * 1;
this.preloadDistance = 1;
}
console.log(`网络类型:${this.networkType},分片大小:${this.chunkSize}`);
}
});
}
/**
* 获取完整音频URL
*/
getFullAudioUrl() {
return wx.surls.server + `/file/fileDownloadWav?filePath=${this.filePath}&meetingId=${this.meetingId}`;
}
/**
* 获取分片音频URL – 优化流式播放
*/
getChunkAudioUrl(start, end) {
const baseUrl = this.getFullAudioUrl();
// 确保start和end是有效的数字,并根据分片数据进行合理赋值
let validStart;
let validEnd;
// 初始化分片偏移数组(如果不存在)
if (!this.chunkOffsets || this.chunkOffsets.length === 0 && this.totalChunks > 0) {
this.initializeChunkOffsets();
}
// 基础验证和默认值设置 – 优化流式播放逻辑
if (typeof start === 'number' && !isNaN(start) && start >= 0) {
validStart = start;
} else {
// 优化:如果没有有效start值,直接根据当前播放时间计算
const currentTime = this.audioContext ? this.audioContext.currentTime : 0;
const targetChunk = this.getChunkIndexByTime(currentTime);
validStart = this.chunkOffsets[targetChunk] || 0;
}
// 优化:确保分片大小合理,不会过大导致超时
const adaptiveChunkSize = this.getAdaptiveChunkSize();
const maxSize = (this.totalSize || 0) – 1;
if (typeof end === 'number' && !isNaN(end) && end >= validStart) {
// 限制end不超过合理范围,避免请求过大
validEnd = Math.min(end, validStart + adaptiveChunkSize – 1, maxSize);
} else {
// 根据网络状况自动调整分片大小
validEnd = Math.min(validStart + adaptiveChunkSize – 1, maxSize);
}
return `${baseUrl}&start=${validStart}&end=${validEnd}`;
}
/**
* 初始化分片偏移量数组
*/
initializeChunkOffsets() {
if (!this.totalSize) return;
this.chunkOffsets = [];
for (let i = 0; i < this.totalChunks; i++) {
this.chunkOffsets[i] = i * this.chunkSize;
}
}
/**
* 初始化播放器 – 优化大文件处理
*/
async initialize() {
if (this.loadingStatus === 'loading') return false;
this.loadingStatus = 'loading';
this.updateStatus('初始化中…');
try {
// 并行获取文件大小和WAV头信息,提高初始化速度
const [totalSize, duration] = await Promise.all([
this.getFileTotalSize(),
this.parseWavHeader()
]);
this.totalSize = totalSize;
this.duration = duration;
// 根据文件大小动态调整分片策略
this.adjustChunkStrategyBasedOnSize();
this.totalChunks = Math.ceil(totalSize / this.chunkSize);
// 初始化分片偏移量数组
this.initializeChunkOffsets();
this.isInitialized = true;
this.loadingStatus = 'ready';
// 确保初始化时同步所有关键状态
this.pageContext.setData({
duration,
totalChunks: this.totalChunks,
showDownloadInfo: true,
fileSize: (totalSize / 1024 / 1024).toFixed(2), // 显示文件大小
isPlaying: false // 明确设置初始播放状态为false,确保状态同步
});
this.updateStatus('点击播放开始');
// 预加载第一个分片,但不阻塞返回
this.downloadChunk(0).catch(err => {
console.warn('预加载第一个分片失败,将在播放时重新尝试:', err);
});
this.currentPlayingChunk = 0;
return true;
} catch (err) {
this.handleError(err.message || '初始化失败');
return false;
}
}
/**
* 根据文件大小调整分片策略
*/
adjustChunkStrategyBasedOnSize() {
// 对于超大文件,使用更小的分片大小和更灵活的加载策略
if (this.totalSize > 100 * 1024 * 1024) { // 100MB以上
this.chunkSize = 1024 * 1024 * 0.5; // 512KB
this.maxCachedChunks = 3; // 减少缓存分片数
this.preloadDistance = 4; // 增加预加载距离
} else if (this.totalSize > 50 * 1024 * 1024) { // 50-100MB
this.chunkSize = 1024 * 1024 * 0.75; // 768KB
}
// 根据网络类型进一步调整
if (['2g', '3g'].includes(this.networkType)) {
this.chunkSize = Math.max(this.minChunkSize, this.chunkSize / 2);
this.preloadDistance = Math.max(1, this.preloadDistance – 1);
}
}
/**
* 根据当前网络状况获取自适应分片大小
*/
getAdaptiveChunkSize() {
// 基础大小
let size = this.chunkSize;
// 根据失败次数动态调整
if (this.failedChunkRanges.size > 0) {
// 如果有多个失败分片,使用最小分片大小
size = this.minChunkSize;
}
// 限制最大分片大小
const maxChunkSize = 2 * 1024 * 1024; // 最大2MB
size = Math.min(size, maxChunkSize);
return size;
}
/**
* 自适应调整分片大小以应对网络问题
*/
adaptiveChunkSize() {
// 当遇到缓冲问题时,减小分片大小
if (this.chunkSize > this.minChunkSize) {
const newSize = Math.max(this.minChunkSize, Math.floor(this.chunkSize * 0.75));
console.log(`自适应调整分片大小: ${this.chunkSize} -> ${newSize}`);
this.chunkSize = newSize;
// 重新计算分片信息
if (this.totalSize > 0) {
this.totalChunks = Math.ceil(this.totalSize / this.chunkSize);
this.initializeChunkOffsets();
}
}
}
// initializeChunkOffsets方法已在上方定义,此处删除重复定义
/**
* 根据时间获取对应的分片索引
*/
getChunkIndexByTime(time) {
if (!this.duration || !this.totalSize || time < 0) return 0;
// 计算时间对应的字节位置
const positionBytes = (time / this.duration) * this.totalSize;
// 使用二分查找确定分片索引
let left = 0;
let right = this.totalChunks – 1;
let result = 0;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
const chunkStart = mid >= 0 && mid < this.chunkOffsets.length ? this.chunkOffsets[mid] : 0;
const chunkEnd = mid === this.totalChunks – 1 ?
this.totalSize : chunkStart + this.getChunkSizeForIndex(mid);
if (positionBytes >= chunkStart && positionBytes < chunkEnd) {
return mid;
} else if (positionBytes < chunkStart) {
right = mid – 1;
} else {
left = mid + 1;
}
}
// 兜底返回,确保不会超出范围
return Math.min(this.totalChunks – 1, Math.max(0, left));
}
/**
* 获取分片大小(考虑失败重试后的缩小策略)
*/
getChunkSizeForIndex(chunkIndex) {
// 失败次数过多时使用最小分片
if (this.failedChunkRanges.get(chunkIndex) >= 3) {
return this.minChunkSize;
}
// 最后一个分片特殊处理,避免超出总大小
if (chunkIndex === this.totalChunks – 1) {
return Math.max(1, this.totalSize – this.chunkOffsets[chunkIndex]);
}
return this.chunkSize;
}
/**
* 检查是否需要切换分片
*/
checkChunkSwitch() {
if (!this.isPlaying || !this.duration || this.isSwitchingChunk || this.currentPlayingChunk === -1) return;
const currentTime = this.audioContext.currentTime || 0;
const currentChunk = this.getChunkIndexByTime(currentTime);
// 如果当前分片索引发生变化,需要切换
if (currentChunk !== this.currentPlayingChunk && currentChunk < this.totalChunks) {
this.isSwitchingChunk = true;
this.switchToNextChunk(currentChunk).finally(() => {
this.isSwitchingChunk = false;
});
}
}
/**
* 获取分片的结束时间
*/
getChunkEndTime(chunkIndex) {
if (!this.totalSize || !this.duration || chunkIndex >= this.totalChunks || chunkIndex < 0) return this.duration || 0;
const start = chunkIndex >= 0 && chunkIndex < this.chunkOffsets.length ? this.chunkOffsets[chunkIndex] : 0;
const size = this.getChunkSizeForIndex(chunkIndex);
const endBytes = Math.min(start + size, this.totalSize);
return (endBytes / this.totalSize) * this.duration;
}
/**
* 切换到下一个分片播放
*/
async switchToNextChunk(nextChunkIndex) {
// 防止重复触发
if (this.isSwitchingChunk) return;
this.isSwitchingChunk = true;
// 移除严格的分片顺序限制,允许更灵活的分片切换
// 但仍然确保nextChunkIndex是有效的
if (nextChunkIndex < 0 || nextChunkIndex >= this.totalChunks) {
console.error(`无效的分片索引: ${nextChunkIndex}`);
this.isSwitchingChunk = false;
return;
}
// 优化:提前显示加载动画
this.isAudioLoading = true;
this.pageContext.setData({ isAudioLoading: true });
try {
if (nextChunkIndex >= this.totalChunks) {
this.handleError('已到达音频末尾');
return;
}
const start = nextChunkIndex >= 0 && nextChunkIndex < this.chunkOffsets.length ? this.chunkOffsets[nextChunkIndex] : 0;
if (start >= this.totalSize) return;
// 优化:分片下载优先级提升
if (!this.downloadedChunks.has(nextChunkIndex)) {
this.updateStatus(`缓冲分片${nextChunkIndex + 1}…`);
try {
// 使用更快的超时设置优先下载当前需要的分片
const originalTimeout = this.requestTimeout;
this.requestTimeout = 15000; // 临时缩短超时时间
await this.downloadChunk(nextChunkIndex);
this.requestTimeout = originalTimeout; // 恢复原始设置
if (!this.isPlaying) return;
} catch (err) {
console.error(`分片${nextChunkIndex + 1}下载失败:`, err);
this.handleError(`分片${nextChunkIndex + 1}下载失败,播放中断`);
return;
}
}
// 计算并保存当前累计播放时间,确保切换切片时时间不重置为00:00
const currentContextTime = this.audioContext.currentTime || 0;
const currentChunkStartTime = this.chunkOffsets[this.currentPlayingChunk] / this.totalSize * this.duration;
this.accumulatedTime = currentChunkStartTime + currentContextTime;
// 优化:更精确的播放位置计算
const targetChunkStartTime = this.chunkOffsets[nextChunkIndex] / this.totalSize * this.duration;
const timeOffset = Math.max(0, this.accumulatedTime – targetChunkStartTime);
this.lastSliceStartTime = targetChunkStartTime;
// 设置新的音频源
const chunkSize = this.getChunkSizeForIndex(nextChunkIndex);
let end = start + chunkSize – 1;
end = Math.min(end, this.totalSize – 1);
// 记录新的当前播放分片
this.currentPlayingChunk = nextChunkIndex;
// 优化:预先停止当前播放,避免音频重叠
try {
if (this.audioContext.paused === false) {
this.audioContext.pause();
}
} catch (pauseErr) {
console.warn('暂停当前音频时出错:', pauseErr);
}
// 清理旧的事件监听器
this.audioContext.offCanplay();
this.audioContext.offPlay();
this.audioContext.offError();
// 设置新的音频源
this.audioContext.src = this.getChunkAudioUrl(start, end);
// 优化:监听音频可以播放的事件,避免过早调用play
await new Promise((resolve, reject) => {
let timeoutId;
const onCanplay = () => {
clearTimeout(timeoutId);
this.audioContext.offCanplay(onCanplay);
resolve();
};
const onError = (err) => {
clearTimeout(timeoutId);
this.audioContext.offCanplay(onCanplay);
this.audioContext.offError(onError);
reject(err);
};
// 设置超时
timeoutId = setTimeout(() => {
this.audioContext.offCanplay(onCanplay);
this.audioContext.offError(onError);
resolve(); // 超时后仍然尝试播放,提高容错性
}, 2000); // 增加超时时间
this.audioContext.onCanplay(onCanplay);
this.audioContext.onError(onError);
});
try {
// 使用play()返回的Promise
await this.audioContext.play();
// 优化:使用微任务队列确保音频已开始播放后再seek
await new Promise(resolve => setTimeout(resolve, 50));
// 设置正确的播放位置
if (timeOffset > 0) {
try {
this.audioContext.seek(timeOffset);
console.log(`切换到分片${nextChunkIndex + 1},跳转位置:${timeOffset}秒`);
} catch (seekErr) {
console.warn('音频跳转失败:', seekErr);
// 即使跳转失败也继续播放,提高容错性
}
}
// 更新页面状态,确保UI显示正确
this.pageContext.setData({
isPlaying: true,
currentTime: this.formatTime(this.accumulatedTime),
progress: (this.accumulatedTime / this.duration) * 100
});
// 确保波形动画持续运行
if (this.pageContext.updateWaveform) {
this.pageContext.updateWaveform();
}
// 预加载下一个分片
this.preloadNextChunks();
} catch (playErr) {
console.error(`播放分片${nextChunkIndex + 1}失败:`, playErr);
// 尝试恢复播放
this.handlePlaybackError();
return;
}
this.updateStatus(`播放分片${nextChunkIndex + 1}`);
} finally {
// 优化:延迟隐藏加载动画,确保切换完成后再隐藏
setTimeout(() => {
this.isAudioLoading = false;
this.pageContext.setData({ isAudioLoading: false });
// 延迟重置切换标志,确保时间显示正确
setTimeout(() => {
this.isSwitchingChunk = false;
}, 200);
// 确保继续预加载后续分片
this.preloadNextChunks();
}, 100); // 减少延迟,提高响应速度
}
}
// 已合并到上面的initAudioContext方法中,此处删除重复定义
detectNetworkType() {
wx.getNetworkType({
success: (res) => {
this.networkType = res.networkType;
this.pageContext.setData({ networkType: res.networkType });
// 智能网络适配策略
switch(this.networkType) {
case '2g':
this.chunkSize = this.minChunkSize;
this.preloadDistance = 1;
break;
case '3g':
this.chunkSize = Math.max(this.minChunkSize, this.chunkSize / 2);
this.preloadDistance = 2;
break;
case '4g':
case '5g':
case 'wifi':
this.chunkSize = Math.min(1024 * 1024 * 3, this.chunkSize * 1.5); // 优质网络可增大分片
this.preloadDistance = Math.min(5, this.preloadDistance + 1);
break;
}
console.log(`网络类型:${this.networkType},分片大小:${this.chunkSize},预加载距离:${this.preloadDistance}`);
}
});
}
getFullAudioUrl() {
return wx.surls.server + `/file/fileDownloadWav?filePath=${this.filePath}&meetingId=${this.meetingId}`;
}
// getChunkAudioUrl方法已在上方定义,此处删除重复定义
async initialize() {
if (this.loadingStatus === 'loading') return false;
this.loadingStatus = 'loading';
this.updateStatus('初始化中…');
try {
const [totalSize, duration] = await Promise.all([
this.getFileTotalSize(),
this.parseWavHeader()
]);
this.totalSize = totalSize;
this.duration = duration;
this.totalChunks = Math.ceil(totalSize / this.chunkSize);
this.isInitialized = true;
this.loadingStatus = 'ready';
// 计算并存储所有切片的时间信息
const chunksTime = this.calculateAllChunksTime();
// 确保直接显示格式化后的时间字符串(00:00或00:00:00格式)
const formattedDuration = this.formatTime(duration);
this.pageContext.setData({
duration: formattedDuration, // 直接使用格式化后的时长显示
durationFormatted: formattedDuration,
totalChunks: this.totalChunks,
showDownloadInfo: true,
chunksTime: chunksTime
});
this.updateStatus('点击播放开始');
await this.downloadChunk(0);
this.currentPlayingChunk = 0;
return true;
} catch (err) {
this.handleError(err.message || '初始化失败');
return false;
}
}
/**
* 修复:正确计算时间对应的分片索引(遍历所有分片,包括未下载的)
*/
getChunkIndexByTime(time) {
if (!this.duration || !this.totalSize || time < 0) return 0;
const positionBytes = (time / this.duration) * this.totalSize;
let cumulativeSize = 0;
// 遍历所有分片(而非仅已下载的),确保索引正确
for (let i = 0; i < this.totalChunks; i++) {
const chunkSize = this.chunkSizes[i] || this.getChunkSizeForIndex(i);
// 检查当前字节是否在当前分片范围内
if (positionBytes >= cumulativeSize && positionBytes < cumulativeSize + chunkSize) {
return i;
}
cumulativeSize += chunkSize;
// 防止累计大小超过总大小导致死循环
if (cumulativeSize >= this.totalSize) {
break;
}
}
// 超出范围时返回最后一个分片
return Math.min(this.totalChunks – 1, Math.floor(this.totalSize / this.chunkSize));
}
checkChunkSwitch() {
if (!this.isPlaying || !this.duration || this.isSwitchingChunk || this.currentPlayingChunk === -1) return;
const currentTime = this.audioContext.currentTime || 0;
const currentChunk = this.getChunkIndexByTime(currentTime);
const currentChunkEndTime = this.getChunkEndTime(currentChunk);
// 修复:确保按顺序播放,防止跳过分片
if (currentChunk < this.currentPlayingChunk) {
// 如果当前时间对应的分片小于正在播放的分片,说明可能发生了跳转
// 这种情况应该交由seek方法处理,不在这里处理
return;
}
// 提前切换:当播放进度超过当前分片的70%时就开始准备下一个分片
const shouldSwitchEarly = currentTime >= currentChunkEndTime * 0.7;
// 紧急切换:当接近分片末尾时立即切换
const shouldSwitchUrgent = currentTime >= currentChunkEndTime – 3;
// 检查下一个分片是否已下载
const nextChunk = currentChunk + 1;
const nextChunkDownloaded = nextChunk < this.totalChunks && this.downloadedChunks.has(nextChunk);
// 根据情况决定切换策略
if (nextChunk < this.totalChunks) {
// 允许更灵活的分片切换,不再严格限制必须按顺序播放
// 但仍然确保不跳转到无效的分片索引
if (nextChunk < 0 || nextChunk >= this.totalChunks) {
console.warn(`尝试切换到无效分片: ${nextChunk}`);
return;
}
// 紧急情况下,立即切换
if (shouldSwitchUrgent) {
this.isSwitchingChunk = true;
this.switchToNextChunk(nextChunk).finally(() => {
this.isSwitchingChunk = false;
});
}
// 预加载未完成时,提前触发下载
else if (shouldSwitchEarly && !nextChunkDownloaded) {
// 确保下一个分片已下载,但不立即切换
this.downloadChunk(nextChunk).catch(err => {
console.warn(`提前下载分片${nextChunk}失败:`, err);
});
}
// 预加载完成后,平滑切换
else if (shouldSwitchEarly && nextChunkDownloaded && this.currentPlayingChunk === currentChunk) {
// 准备切换,但等待更合适的时机
setTimeout(() => {
if (this.isPlaying && !this.isSwitchingChunk && this.currentPlayingChunk === currentChunk) {
this.isSwitchingChunk = true;
this.switchToNextChunk(nextChunk).finally(() => {
this.isSwitchingChunk = false;
});
}
}, 500);
}
}
// 持续预加载,确保流畅播放
this.preloadNextChunks();
}
/**
* 修复:准确计算分片结束时间(基于实际边界)
*/
getChunkEndTime(chunkIndex) {
const start = this.chunkOffsets[chunkIndex];
const size = this.getChunkSizeForIndex(chunkIndex);
const endBytes = Math.min(start + size, this.totalSize);
return (endBytes / this.totalSize) * this.duration;
}
getFileTotalSize() {
return new Promise((resolve, reject) => {
wx.request({
url: wx.surls.server + `/file/getFileSize?meetingId=${this.meetingId}`,
method: 'GET',
timeout: this.requestTimeout,
success: (res) => {
if (res.data.data.size && typeof res.data.data.size === 'number') {
//大小
this.totalSize = res.data.data.size;
//文件总时长接口返回值就是秒
// 安全地将字符串转换为数字并格式化
const milliseconds = res.data.data.milliseconds;
this.currentTime = typeof milliseconds === 'string' ? parseFloat(milliseconds) : (typeof milliseconds === 'number' ? milliseconds : 0);
// 确保时间值有效
if (isNaN(this.currentTime) || this.currentTime < 0) {
this.currentTime = 0;
}
// 同步更新页面时长显示
if (this.pageContext && typeof this.pageContext.setData === 'function') {
this.pageContext.setData({
currentTime: this.formatTime(this.currentTime)
});
}
resolve(this.totalSize);
} else {
reject(new Error(`文件大小接口异常:${JSON.stringify(res.data)}`));
}
},
fail: (err) => reject(new Error(`获取文件大小失败:${err.errMsg}`))
});
});
}
parseWavHeader() {
return new Promise((resolve, reject) => {
const headerUrl = this.getChunkAudioUrl(0, 43);
wx.request({
url: headerUrl,
method: 'GET',
responseType: 'arraybuffer',
timeout: this.requestTimeout,
success: (res) => {
if (res.statusCode !== 200 && res.statusCode !== 206) {
reject(new Error(`获取WAV头失败,状态码:${res.statusCode}`));
return;
}
try {
const dataView = new DataView(res.data);
const riff = String.fromCharCode(…[0,1,2,3].map(i => dataView.getUint8(i)));
const wave = String.fromCharCode(…[8,9,10,11].map(i => dataView.getUint8(i)));
if (riff !== 'RIFF' || wave !== 'WAVE') {
reject(new Error('不是有效的WAV文件'));
return;
}
let byteRate = 0;
let dataSize = 0;
let offset = 12;
while (offset + 8 <= res.data.byteLength) {
const subchunkId = String.fromCharCode(…[0,1,2,3].map(i => dataView.getUint8(offset + i)));
const subchunkSize = dataView.getUint32(offset + 4, true);
if (subchunkId === 'fmt ') {
byteRate = dataView.getUint32(offset + 20, true);
offset += 8 + subchunkSize;
} else if (subchunkId === 'data') {
dataSize = subchunkSize;
break;
} else {
offset += 8 + subchunkSize;
}
}
if (!byteRate || !dataSize) reject(new Error('解析WAV参数失败'));
resolve(dataSize / byteRate);
} catch (e) {
reject(new Error(`解析WAV头失败:${e.message}`));
}
},
fail: (err) => reject(new Error(`请求WAV头失败:${err.errMsg}`))
});
});
}
async downloadChunk(chunkIndex) {
// 流式播放优化:不缓存所有分片数据,而是按需获取
const start = chunkIndex >= 0 && chunkIndex < this.chunkOffsets.length ? this.chunkOffsets[chunkIndex] : 0;
if (start >= this.totalSize) return;
// 对于超大文件,使用动态分片大小
const currentChunkSize = this.getAdaptiveChunkSize();
let end = start + currentChunkSize – 1;
end = Math.min(end, this.totalSize – 1); // 确保不超过总大小
this.updateStatus(`加载分片 ${chunkIndex + 1}/${this.totalChunks}`);
try {
// 对于流式播放,不需要将所有分片数据存储在内存中
// 但仍需要记录哪些分片已下载以避免重复请求
this.downloadedChunks.set(chunkIndex, true); // 只存储标记,不存储数据
// 清除旧分片数据,保持内存使用合理
this.checkAndCleanOldChunks();
// 不需要等待实际数据,直接返回
this.failedChunkRanges.delete(chunkIndex);
// 通知页面更新下载状态
this.pageContext.setData({
downloadedChunks: Array.from(this.downloadedChunks.keys())
});
} catch (err) {
const failCount = (this.failedChunkRanges.get(chunkIndex) || 0) + 1;
this.failedChunkRanges.set(chunkIndex, failCount);
// 失败时立即减小分片大小
if (failCount >= 2) {
this.adaptiveChunkSize();
}
const msg = err.message.includes('timeout') ? `加载超时(已失败${failCount}次)` : '加载失败';
this.updateStatus(`分片 ${chunkIndex + 1} ${msg}`);
console.error(`分片${chunkIndex}(${start}-${end})错误:`, err);
throw err;
}
}
/**
* 检查并清理不再需要的分片数据,实现LRU缓存
*/
checkAndCleanOldChunks() {
if (this.downloadedChunks.size <= this.maxCachedChunks) return;
// 找出需要保留的分片索引
const keepChunks = new Set();
const currentIdx = this.currentPlayingChunk;
// 保留当前播放分片和预加载分片
for (let i = Math.max(0, currentIdx – 1);
i <= Math.min(this.totalChunks – 1, currentIdx + this.preloadDistance);
i++) {
keepChunks.add(i);
}
// 删除不在保留列表中的分片
const chunksToDelete = [];
for (const chunkIndex of this.downloadedChunks.keys()) {
if (!keepChunks.has(chunkIndex)) {
chunksToDelete.push(chunkIndex);
}
}
// 执行删除
chunksToDelete.forEach(idx => {
this.downloadedChunks.delete(idx);
if (this.chunkSizes[idx]) {
this.chunkSizes[idx] = undefined; // 释放内存
}
});
// 通知页面更新
if (chunksToDelete.length > 0) {
this.pageContext.setData({
downloadedChunks: Array.from(this.downloadedChunks.keys())
});
}
}
requestChunkWithRetry(start, end, retry, currentChunkSize) {
return new Promise((resolve, reject) => {
const chunkUrl = this.getChunkAudioUrl(start, end);
console.log(`请求分片[${retry}](${(currentChunkSize/1024/1024).toFixed(2)}MB):${chunkUrl}`);
wx.request({
url: chunkUrl,
method: 'GET',
responseType: 'arraybuffer',
timeout: this.requestTimeout,
success: (res) => {
if ((res.statusCode === 200 || res.statusCode === 206) && res.data) {
resolve(res.data);
} else {
this.handleRetry(start, end, retry, currentChunkSize, `状态码${res.statusCode}`, resolve, reject);
}
},
fail: (err) => {
const msg = err.errMsg.includes('timeout') ? '超时' : err.errMsg;
this.handleRetry(start, end, retry, currentChunkSize, msg, resolve, reject);
}
});
});
}
handleRetry(start, end, retry, currentChunkSize, errorMsg, resolve, reject) {
if (retry < this.retryCount) {
const baseDelay = errorMsg.includes('超时') ? 2000 : 1000;
const delay = baseDelay * Math.pow(2, retry);
console.log(`分片${start}-${end}${errorMsg},${delay}ms后重试(${retry + 1}/${this.retryCount})`);
setTimeout(() => {
this.requestChunkWithRetry(start, end, retry + 1, currentChunkSize).then(resolve).catch(reject);
}, delay);
} else {
reject(new Error(`已达最大重试次数:${errorMsg}`));
}
}
preloadNextChunks() {
if (!this.isInitialized || !this.duration || !this.isPlaying || this.currentPlayingChunk === -1) return;
// 优化:根据播放位置和网络动态调整预加载策略
const currentTime = this.audioContext.currentTime || 0;
const remainingTime = this.duration – currentTime;
const isFastForwarding = currentTime > this.lastPlayTime + 2; // 检测快进操作
// 根据播放速度和剩余时间调整预加载优先级
const adaptivePreloadDistance = isFastForwarding ?
Math.min(8, this.preloadDistance + 2) :
this.preloadDistance;
// 高优先级:预加载即将播放的分片
for (let i = 1; i <= adaptivePreloadDistance; i++) {
const nextChunk = this.currentPlayingChunk + i;
// 确保分片索引有效
if (nextChunk >= 0 && nextChunk < this.totalChunks) {
// 检查是否已经标记为下载
if (!this.downloadedChunks.has(nextChunk)) {
// 立即预加载,不延迟
this.downloadChunk(nextChunk).catch(err => {
console.warn(`预加载分片${nextChunk}失败,将在需要时重新尝试:`, err);
// 失败后快速重试一次
setTimeout(() => {
if (this.isPlaying && !this.downloadedChunks.has(nextChunk)) {
this.downloadChunk(nextChunk).catch(() => {});
}
}, 500);
});
}
}
}
// 优化:预加载上一个分片,支持回退操作
const prevChunk = this.currentPlayingChunk – 1;
if (prevChunk >= 0 && !this.downloadedChunks.has(prevChunk)) {
this.downloadChunk(prevChunk).catch(() => {});
}
// 对于超大文件,更积极地预加载
if (this.totalSize > 30 * 1024 * 1024) {
// 中等优先级:提前预加载
for (let i = adaptivePreloadDistance + 1; i <= adaptivePreloadDistance + 3; i++) {
const midChunk = this.currentPlayingChunk + i;
if (midChunk < this.totalChunks && !this.downloadedChunks.has(midChunk)) {
// 延迟预加载,避免网络拥塞
setTimeout(() => {
if (this.isPlaying && !this.downloadedChunks.has(midChunk)) {
this.downloadChunk(midChunk).catch(() => {});
}
}, 300 * (i – adaptivePreloadDistance));
}
}
// 低优先级:远程预加载
if (remainingTime > 60 && ['wifi', '5g', '4g'].includes(this.networkType)) {
// 在网络良好且剩余时间充足时,预加载更多分片
for (let i = adaptivePreloadDistance + 4; i <= adaptivePreloadDistance + 6; i++) {
const farChunk = this.currentPlayingChunk + i;
if (farChunk < this.totalChunks && !this.downloadedChunks.has(farChunk)) {
setTimeout(() => {
if (this.isPlaying && !this.downloadedChunks.has(farChunk)) {
this.downloadChunk(farChunk).catch(() => {});
}
}, 1000 * (i – adaptivePreloadDistance));
}
}
}
}
// 更新最后播放时间,用于检测快进
this.lastPlayTime = currentTime;
}
async downloadAndMergeAllChunks() {
this.updateStatus('正在下载完整音频…');
const tempDir = wx.env.USER_DATA_PATH;
const localFilePath = `${tempDir}/${Date.now()}.wav`;
try {
const allChunks = [];
for (let i = 0; i < this.totalChunks; i++) {
if (!this.downloadedChunks.has(i)) {
await this.downloadChunk(i);
}
allChunks.push(this.downloadedChunks.get(i));
}
const totalLength = allChunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
const mergedBuffer = new Uint8Array(totalLength);
let offset = 0;
allChunks.forEach(chunk => {
mergedBuffer.set(new Uint8Array(chunk), offset);
offset += chunk.byteLength;
});
await wx.getFileSystemManager().writeFile({
filePath: localFilePath,
data: mergedBuffer.buffer,
encoding: 'binary'
});
this.localFilePath = localFilePath;
this.updateStatus('下载完成,准备播放');
return true;
} catch (err) {
this.handleError(`合并失败:${err.message}`);
return false;
}
}
// 启动进度更新定时器
startProgressInterval() {
// 清除可能存在的旧定时器
this.clearProgressInterval();
// 设置新的定时器,每60ms更新一次进度,提高时间显示流畅度
this.progressInterval = setInterval(() => {
if (this.isPlaying) {
this.updatePlayProgress();
} else {
this.clearProgressInterval();
}
}, 60);
}
// 清除进度更新定时器
clearProgressInterval() {
if (this.progressInterval) {
clearInterval(this.progressInterval);
this.progressInterval = null;
}
}
updatePlayProgress() {
// 添加完整的边界检查,确保必要的属性存在
if (!this.currentTime) {
return;
}
let accumulatedTime = 0;
let safeProgress = 0;
// 只有在播放状态下才计算累计时间
if (this.isPlaying && this.audioContext && typeof this.audioContext.currentTime === 'number') {
// 优化累计时间计算逻辑
if (this.currentPlayingChunk >= 0 && this.chunkOffsets && Array.isArray(this.chunkOffsets)) {
// 计算当前切片的开始时间,增加除以零保护
const currentChunkStartTime = (this.chunkOffsets[this.currentPlayingChunk] || 0) / Math.max(1, this.totalSize) * this.currentTime;
if (this.isSwitchingChunk && this.accumulatedTime > 0) {
// 切换切片过程中,优先使用累计时间,确保时间连续性
accumulatedTime = this.accumulatedTime;
} else {
// 正常播放时,精确计算累计时间
const audioTime = this.audioContext.currentTime || 0;
accumulatedTime = currentChunkStartTime + audioTime;
}
} else if (this.accumulatedTime > 0) {
// 回退方案:如果无法计算切片时间,使用最后保存的累计时间
accumulatedTime = this.accumulatedTime;
} else {
// 安全回退:确保至少有一个有效的时间值
accumulatedTime = 0;
}
// 严格确保时间在有效范围内,防止进度条异常
accumulatedTime = Math.max(0, Math.min(accumulatedTime, this.currentTime));
// 更新累计时间
this.accumulatedTime = accumulatedTime;
// 计算进度百分比,确保结果是有效的数字
const progress = (accumulatedTime / Math.max(1, this.currentTime)) * 100;
safeProgress = Math.max(0, Math.min(100, progress));
}
try {
// 使用安全的状态更新,确保页面数据的准确性
if (this.pageContext && typeof this.pageContext.setData === 'function') {
this.pageContext.setData({
currentTime: this.formatTime(this.currentTime), // 显示格式化后的文件总时长
duration: this.isPlaying ? this.formatTime(accumulatedTime) : “00:00”, // 未播放时显示00:00
progress: safeProgress, // 确保进度值在0-100之间
currentPlayingChunk: this.currentPlayingChunk || -1
});
}
} catch (error) {
console.warn('更新页面播放进度时出错:', error);
}
// 只有在非切换状态时才检查切片切换,避免重复触发
if (!this.isSwitchingChunk) {
this.checkChunkSwitch();
}
}
async togglePlay() {
// 清除可能存在的旧定时器,确保不会有多个定时器同时运行
this.clearProgressInterval();
// 先同步内部状态,再更新页面状态,确保一致性
const newIsPlaying = !this.isPlaying;
this.isPlaying = newIsPlaying;
this.pageContext.setData({
isPlaying: newIsPlaying,
currentTime: this.formatTime(this.currentTime),
duration: this.formatTime(this.accumulatedTime || 0)
});
if (!this.isInitialized) {
// 初始化时显示加载动画
this.isAudioLoading = true;
this.pageContext.setData({ isAudioLoading: true });
const success = await this.initialize();
// 初始化完成后隐藏加载动画
setTimeout(() => {
this.isAudioLoading = false;
this.pageContext.setData({ isAudioLoading: false });
}, 200);
if (!success) return;
}
// 防止在切换过程中重复触发
if (this.isSwitchingChunk) return;
this.isSwitchingChunk = true;
try {
// 调试日志:打印当前isPlaying的实际状态值
console.log('togglePlay: 当前播放状态:', this.isPlaying);
if (this.isPlaying === false) {
// 暂停播放时确保状态正确更新
if (this.audioContext) {
const currentContextTime = this.audioContext.currentTime || 0;
// 计算并保存累计播放时间,使用更健壮的计算方式
if (this.currentPlayingChunk >= 0 && this.chunkOffsets && this.totalSize > 0) {
const currentChunkStartTime = (this.chunkOffsets[this.currentPlayingChunk] || 0) / this.totalSize * this.currentTime;
this.accumulatedTime = Math.max(0, currentChunkStartTime + currentContextTime);
// 确保累计时间不超过总时长
this.accumulatedTime = Math.min(this.accumulatedTime, this.currentTime);
}
// 保存当前切片的相对时间
this.lastPlayTime = currentContextTime;
// 关键修复:保存当前播放的切片索引,确保恢复播放时使用相同切片
this.savedPlayingChunk = this.currentPlayingChunk;
console.log('暂停时保存的切片索引:', this.savedPlayingChunk);
// 确保音频暂停成功
try {
this.audioContext.pause();
} catch (err) {
console.warn('暂停音频时出错:', err);
}
// 暂停时清除进度更新定时器
this.clearProgressInterval();
}
// 确保状态一致性
this.isPlaying = false;
// 立即更新页面状态,避免状态不同步,使用更安全的方式
try {
if (this.pageContext && typeof this.pageContext.setData === 'function') {
const safeAccumulatedTime = this.accumulatedTime || 0;
const safeProgress = this.currentTime > 0 ? (safeAccumulatedTime / this.currentTime) * 100 : 0;
this.pageContext.setData({
isPlaying: false,
currentTime: this.formatTime(this.currentTime), // 显示格式化后的文件总时长
duration: this.formatTime(safeAccumulatedTime), // 显示格式化后的累计时间
progress: Math.max(0, Math.min(100, safeProgress)) // 同步更新进度条
});
}
} catch (e) {
console.warn('更新页面状态失败:', e);
}
} else {
// 显示加载动画,使用安全的状态更新
this.isAudioLoading = true;
try {
if (this.pageContext && typeof this.pageContext.setData === 'function') {
this.pageContext.setData({ isAudioLoading: true });
}
} catch (e) {
console.warn('更新加载状态失败:', e);
}
// 清除旧的进度更新定时器
this.clearProgressInterval();
// 使用累计时间来确定播放位置,确保时间连续性,增强优先级逻辑
let targetTime = 0;
if (this.accumulatedTime > 0) {
// 最高优先级:使用累计时间
targetTime = this.accumulatedTime;
} else if (this.lastPlayTime > 0 && this.currentPlayingChunk >= 0 && this.chunkOffsets && this.totalSize > 0) {
// 次高优先级:结合lastPlayTime和当前切片位置计算
const currentChunkStartTime = (this.chunkOffsets[this.currentPlayingChunk] || 0) / this.totalSize * this.currentTime;
targetTime = currentChunkStartTime + this.lastPlayTime;
} else {
// 默认从0开始
targetTime = 0;
}
// 确保目标时间有效
targetTime = Math.max(0, Math.min(targetTime, this.currentTime || 0));
// 关键修复:优先使用保存的切片索引,而不是仅基于时间计算新的切片索引
let targetChunk;
if (this.savedPlayingChunk >= 0 && this.savedPlayingChunk < this.totalChunks) {
// 优先使用暂停时保存的切片索引
targetChunk = this.savedPlayingChunk;
console.log('恢复播放使用保存的切片索引:', targetChunk);
} else {
// 回退方案:根据累计时间计算目标分片
targetChunk = this.getChunkIndexByTime(targetTime);
console.log('恢复播放使用计算的切片索引:', targetChunk);
}
this.currentPlayingChunk = targetChunk;
// 清除保存的索引,防止重复使用
this.savedPlayingChunk = -1;
// 计算分片边界
const start = this.chunkOffsets[this.currentPlayingChunk] || 0;
const chunkSize = this.getChunkSizeForIndex(this.currentPlayingChunk);
let end = start + chunkSize – 1;
end = Math.min(end, this.totalSize – 1);
// 设置音频源为当前分片URL
this.audioContext.src = this.getChunkAudioUrl(start, end);
// 标记当前分片为已加载
this.downloadedChunks.set(this.currentPlayingChunk, true);
try {
// 先设置事件监听器,再播放
const playPromise = new Promise((resolve, reject) => {
const onPlay = () => {
this.audioContext.offPlay(onPlay);
this.audioContext.offError(onError);
resolve();
};
const onError = (err) => {
this.audioContext.offPlay(onPlay);
this.audioContext.offError(onError);
reject(err);
};
this.audioContext.onPlay(onPlay);
this.audioContext.onError(onError);
// 播放音频,增加对play()返回值的兼容性检查
try {
const playResult = this.audioContext.play();
// 更安全地检查playResult是否为Promise类型
if (playResult && typeof playResult === 'object' && typeof playResult.then === 'function') {
// 安全地处理playResult的catch
try {
playResult.catch(reject);
} catch (catchErr) {
// 如果catch调用失败,仍然继续执行
console.warn('playResult.catch调用失败:', catchErr);
}
}
} catch (err) {
reject(err);
}
});
// 等待播放成功
await playPromise;
// 调试日志:播放成功,准备更新状态
console.log('togglePlay: 播放成功,准备更新状态');
// 确保播放状态一致
this.isPlaying = true;
console.log('togglePlay: 内部isPlaying已设置为:', this.isPlaying);
// 立即更新页面状态,避免状态不同步
try {
this.pageContext.setData({
isPlaying: true,
currentTime: this.formatTime(this.currentTime),
duration: this.formatTime(this.accumulatedTime || 0)
});
console.log('togglePlay: 页面isPlaying状态已更新为:true');
} catch (e) {
console.warn('更新页面状态失败:', e);
}
// 启动进度更新定时器
this.startProgressInterval();
// 启动波形动画
if (this.pageContext.updateWaveform) {
this.pageContext.updateWaveform();
}
// 如果需要跳转,在play之后seek
// 计算当前切片的开始时间
const currentChunkStartTime = this.chunkOffsets[this.currentPlayingChunk] / this.totalSize * this.currentTime;
// 计算需要seek的相对时间(累计时间减去当前切片开始时间)
const seekTime = Math.max(0, targetTime – currentChunkStartTime);
if (seekTime > 0) {
// 延迟seek,确保音频已经开始加载
setTimeout(() => {
if (this.isPlaying) {
try {
this.audioContext.seek(seekTime);
console.log(`播放开始,从累计时间 ${targetTime} 秒跳转,相对切片位置 ${seekTime} 秒`);
} catch (seekErr) {
console.warn('音频跳转失败:', seekErr);
}
}
}, 100);
}
// 异步预加载下一个分片
this.preloadNextChunks();
console.log('开始播放,从时间点:', targetTime, ',分片:', targetChunk);
} catch (err) {
// 播放失败时尝试降级策略
this.isPlaying = false;
// 清除进度更新定时器
this.clearProgressInterval();
// 确保错误时状态也能正确同步
try {
this.pageContext.setData({
isPlaying: false,
currentTime: this.formatTime(this.currentTime),
duration: this.formatTime(this.accumulatedTime || 0)
});
} catch (e) {
console.warn('更新页面状态失败:', e);
}
console.error('播放失败:', err);
console.log('播放失败,状态已更新为:false');
this.handlePlaybackError();
}
}
} finally {
// 隐藏加载动画并重置切换标志
setTimeout(() => {
this.isAudioLoading = false;
this.pageContext.setData({ isAudioLoading: false });
this.isSwitchingChunk = false;
}, 200); // 短暂延迟以确保动画效果可见
}
}
/**
* 处理播放错误,尝试恢复播放
*/
async handlePlaybackError() {
// 避免在切换过程中触发错误恢复,防止状态冲突
if (this.isSwitchingChunk) return;
// 确保状态一致性
const wasPlaying = this.isPlaying;
this.isPlaying = false;
// 立即更新页面状态
try {
this.pageContext.setData({ isPlaying: false });
console.log('播放错误恢复,状态已更新为:false');
} catch (e) {
console.warn('更新页面状态失败:', e);
}
// 设置切换标志,防止在恢复过程中触发其他操作
this.isSwitchingChunk = true;
try {
this.updateStatus('播放出错,尝试恢复…');
// 立即减小分片大小
this.adaptiveChunkSize();
// 尝试使用更小的分片重新播放当前位置
const currentTime = this.audioContext.currentTime || this.lastPlayTime || 0;
// 修复:错误恢复时严格确保使用当前播放的分片,不允许跳转到其他分片
// 这样可以保证即使在错误恢复情况下也严格按照分片顺序播放
const targetChunk = this.currentPlayingChunk;
// 安全检查,确保targetChunk有效
if (targetChunk === -1 || targetChunk >= this.totalChunks) {
console.error('当前播放分片无效,尝试获取当前时间对应的分片');
const fallbackChunk = this.getChunkIndexByTime(currentTime);
// 仍然只允许使用当前分片或回退到0(初始分片)
this.currentPlayingChunk = fallbackChunk === 0 ? 0 : this.currentPlayingChunk;
return;
}
console.log(`错误恢复中,使用当前分片 ${targetChunk} 恢复播放`);
try {
// 先暂停,避免可能的播放冲突
this.audioContext.pause();
// 使用最小分片大小重新播放
const start = this.chunkOffsets[targetChunk] || 0;
const minSize = this.minChunkSize;
let end = start + minSize – 1;
end = Math.min(end, this.totalSize – 1);
this.audioContext.src = this.getChunkAudioUrl(start, end);
await this.audioContext.play();
// 尝试定位到出错时的位置
setTimeout(() => {
if (this.isPlaying) {
this.audioContext.seek(currentTime);
}
}, 100);
this.updateStatus('已恢复播放');
console.log('播放错误已恢复,从时间点:', currentTime);
} catch (err) {
// 如果恢复失败,更新状态并停止播放
this.isPlaying = false;
this.pageContext.setData({ isPlaying: false });
this.handleError(`无法恢复播放:${err.message}`);
}
} finally {
// 确保无论如何都会重置切换标志
setTimeout(() => {
this.isSwitchingChunk = false;
}, 100);
}
}
getCurrentBytePosition() {
if (!this.currentTime || !this.totalSize) return 0;
const currentTime = this.audioContext.currentTime || 0;
return Math.floor((currentTime / this.currentTime) * this.totalSize);
}
getPlayUrlWithRange() {
const start = this.getCurrentBytePosition();
let end = start + this.chunkSize – 1;
end = Math.min(end, this.totalSize – 1);
return this.getChunkAudioUrl(start, end);
}
stop() {
try {
// 确保停止时的状态一致性
this.isSwitchingChunk = true;
// 清除进度更新定时器
this.clearProgressInterval();
if (this.audioContext) {
try {
this.audioContext.pause(); // 先暂停
this.audioContext.stop(); // 再停止
} catch (e) {
console.error('停止音频失败:', e);
}
}
// 重置所有播放状态
this.isPlaying = false;
this.lastPlayTime = 0;
this.currentPlayingChunk = -1;
this.savedPlayingChunk = -1;
this.accumulatedTime = 0;
this.duration = 0;
// 更新UI状态
if (this.pageContext && typeof this.pageContext.setData === 'function') {
this.pageContext.setData({
isPlaying: false,
currentTime: this.formatTime(this.currentTime), // 显示格式化后的文件总时长
duration: '00:00', // 重置累计时长显示
progress: 0
});
}
console.log('播放已完全停止并重置');
} finally {
// 确保无论如何都会重置切换标志
setTimeout(() => {
this.isSwitchingChunk = false;
}, 100);
}
}
async seek(position) {
// 增强输入验证和安全检查
if (!this.isInitialized || typeof position !== 'number' || isNaN(position)) {
console.warn('无效的seek位置参数');
return;
}
// 确保位置在有效范围内
const safePosition = Math.max(0, Math.min(position, this.currentTime || 0));
// 防止在切换过程中重复触发
if (this.isSwitchingChunk) {
console.warn('正在切换切片,跳过seek操作');
return;
}
this.isSwitchingChunk = true;
try {
// 显示加载动画,使用安全的状态更新
this.isAudioLoading = true;
try {
if (this.pageContext && typeof this.pageContext.setData === 'function') {
this.pageContext.setData({ isAudioLoading: true });
}
} catch (e) {
console.warn('更新加载状态失败:', e);
}
// 更新累计时间为安全的位置值
this.accumulatedTime = safePosition;
this.duration = safePosition; // 更新切片累计时长
// 计算在目标切片中的相对时间,添加更安全的计算逻辑
const targetChunk = this.getChunkIndexByTime(safePosition);
// 安全计算目标切片的开始时间
const targetChunkStartTime = this.chunkOffsets && this.chunkOffsets[targetChunk] && this.totalSize > 0
? (this.chunkOffsets[targetChunk] / this.totalSize) * this.currentTime
: 0;
// 计算并保存相对时间,确保非负
this.lastPlayTime = Math.max(0, safePosition – targetChunkStartTime);
// 同步更新UI显示,确保拖动后立即反馈
try {
if (this.pageContext && typeof this.pageContext.setData === 'function') {
const safeProgress = this.currentTime > 0 ? (safePosition / this.currentTime) * 100 : 0;
this.pageContext.setData({
currentTime: this.formatTime(this.currentTime), // 显示文件总时长
duration: this.formatTime(safePosition), // 显示累计时长
progress: Math.max(0, Math.min(100, safeProgress))
});
}
} catch (e) {
console.warn('更新UI进度显示失败:', e);
}
// 允许跳转到任何有效分片,不再限制分片顺序
const chunkDifference = Math.abs(targetChunk – (this.currentPlayingChunk || 0));
// 仅检查分片索引有效性,不再限制跳转范围
if (targetChunk < 0 || targetChunk >= this.totalChunks) {
console.error(`无效的分片索引: ${targetChunk}`);
this.isAudioLoading = false;
this.pageContext.setData({ isAudioLoading: false });
this.isSwitchingChunk = false;
return;
}
// 记录非相邻分片跳转,但允许继续执行
if (this.currentPlayingChunk !== -1 && chunkDifference > 1) {
console.log(`跳转到非相邻分片: 当前分片${this.currentPlayingChunk}, 目标分片${targetChunk}`);
}
this.currentPlayingChunk = targetChunk;
// 清理部分缓存的分片,为新位置的分片腾出空间
this.checkAndCleanOldChunks();
// 计算分片边界
const start = this.chunkOffsets[targetChunk] || 0;
const adaptiveSize = this.getAdaptiveChunkSize();
let end = start + adaptiveSize – 1;
end = Math.min(end, this.totalSize – 1);
// 立即设置音频源,不等待下载完成
this.audioContext.src = this.getChunkAudioUrl(start, end);
this.downloadedChunks.set(targetChunk, true);
this.updateStatus(`定位到 ${this.formatTime(position)}`);
try {
// 尝试播放并定位
await this.audioContext.play();
// 延迟seek以确保音频已经开始加载
setTimeout(() => {
if (this.isPlaying) {
this.audioContext.seek(position);
}
}, 100);
// 更新播放状态
this.isPlaying = true;
this.pageContext.setData({ isPlaying: true });
this.updatePlayProgress();
// 开始预加载新位置附近的分片
this.preloadNextChunks();
console.log('已跳转到时间点:', position);
} catch (err) {
// 跳转失败时尝试使用更小的分片
this.isPlaying = false;
this.pageContext.setData({ isPlaying: false });
this.handleSeekError(position, targetChunk);
}
} catch (err) {
this.handleError(`跳转失败:${err.message}`);
} finally {
// 隐藏加载动画并重置切换标志
setTimeout(() => {
this.isAudioLoading = false;
this.pageContext.setData({ isAudioLoading: false });
this.isSwitchingChunk = false;
}, 200); // 短暂延迟以确保动画效果可见
}
}
/**
* 处理跳转错误,尝试使用更小的分片
*/
async handleSeekError(position, targetChunk) {
this.updateStatus('定位失败,尝试使用更小分片…');
// 显示加载动画
this.isAudioLoading = true;
this.pageContext.setData({ isAudioLoading: true });
// 使用最小分片大小重新尝试
this.adaptiveChunkSize();
const start = this.chunkOffsets[targetChunk] || 0;
const minSize = this.minChunkSize;
let end = start + minSize – 1;
end = Math.min(end, this.totalSize – 1);
try {
this.audioContext.src = this.getChunkAudioUrl(start, end);
await this.audioContext.play();
setTimeout(() => {
if (this.isPlaying) {
this.audioContext.seek(position);
}
}, 100);
this.updateStatus('已定位');
} catch (err) {
this.handleError(`无法定位:${err.message}`);
} finally {
// 隐藏加载动画
setTimeout(() => {
this.isAudioLoading = false;
this.pageContext.setData({ isAudioLoading: false });
}, 200);
}
}
/**
* 格式化时间显示 – 支持时分秒格式
*/
formatTime(seconds) {
if (isNaN(seconds) || seconds < 0) return '00:00:00';
const hours = Math.floor(seconds / 3600);
const mins = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
if (hours > 0) {
return `${hours.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
} else {
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}
}
/**
* 计算所有切片的时间信息
* 返回一个包含每个切片开始时间和结束时间的数组
*/
calculateAllChunksTime() {
if (!this.totalSize || !this.currentTime || !this.totalChunks) return [];
const chunksTime = [];
let cumulativeSize = 0;
for (let i = 0; i < this.totalChunks; i++) {
const chunkSize = this.chunkSizes[i] || this.getChunkSizeForIndex(i);
const startBytes = cumulativeSize;
const endBytes = Math.min(startBytes + chunkSize, this.totalSize);
// 计算切片的开始和结束时间
const startTime = (startBytes / this.totalSize) * this.currentTime;
const endTime = (endBytes / this.totalSize) * this.currentTime;
chunksTime.push({
index: i,
startTime: startTime,
endTime: endTime,
startTimeFormatted: this.formatTime(startTime),
endTimeFormatted: this.formatTime(endTime),
size: endBytes – startBytes
});
cumulativeSize = endBytes;
// 防止累计大小超过总大小
if (cumulativeSize >= this.totalSize) {
break;
}
}
return chunksTime;
}
updateStatus(text) {
this.pageContext.setData({ statusText: text });
}
handleError(message) {
this.loadingStatus = 'error';
this.isPlaying = false;
this.pageContext.setData({
statusText: `错误:${message}`,
isPlaying: false
});
console.error('播放器错误:', message);
}
destroy() {
// 完善资源清理
if (this.loadingTimer) {
clearTimeout(this.loadingTimer);
this.loadingTimer = null;
}
// 清除进度更新定时器
this.clearProgressInterval();
if (this.audioContext) {
this.audioContext.stop();
this.audioContext.destroy();
this.audioContext = null;
}
// 清理所有缓存数据
this.downloadedChunks.clear();
this.chunkSizes = [];
this.chunkOffsets = [];
this.failedChunkRanges.clear();
// 重置状态
this.isPlaying = false;
this.isInitialized = false;
this.currentPlayingChunk = -1;
console.log('播放器已销毁,资源已释放');
}
}
module.exports = OptimizedWavPlayer;
© 版权声明
文章版权归作者所有,未经允许请勿转载。
暂无评论...