微信小程序切片获取语音文件

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;

© 版权声明

相关文章

暂无评论

none
暂无评论...