底层API实现Audio元素

公司IOS APP兼容性问题

大家第一反应是,用h5audio元素就好了,何须自己实现。原因只有一个:audio元素出问题了,在公司IOS APP中出现了兼容性问题:

<audio preload="auto" id="videoPlay">
  <source src={declarationUrl}/>
</audio>
  • 列表页 > 详情页 详情页有个人宣言的短音频,点击播放。功能很简单。
  • 到不同的详情页播放,时间久了(无规律)。就会出现播放不了的情况。
  • 卸载重装APP正常,用久了再次出现。
  • 尝试抓包,看上去是缓存了错误的音频(长度为0)。ios排查不出问题,坚称缓存机制没有问题。
  • 压力之下,实在没办法,在小组架构的指导下,才知道还有底层API。

解决方案

排查下来,大概率还是 <audio> 自身的缓存策略和我们的 IOS webview 的缓存策略有点冲突。决定自己实现一个<audio>,不采用缓存策略,音频完全下载以后调用底层 API 来实现播放。

组件代码:

class AudioBuffer {

  constructor(props) {
    this.url = props.url || '';  // 音频url
    this.playBtn = props.playBtn;  // 播放/暂停 按钮
    this.stopBtn = props.stopBtn;  // 停止 按钮
    this.callback = props.callback; // 回调函数 param:play|pause|stop
    this.sourceNode = null; // BufferSource对象节点
    this.startedAt = 0;  // 播放时间点
    this.pausedAt = 0;  // 暂停时间点
    this.playing = false;  // 播放中
    ['play', 'pause', 'stop', 'update', 'getCurrentTime', 'getDuration', 'init', 'bind'].forEach(method => {
      this[method] = this[method].bind(this);
    });
    this.init();
  }
  // 播放
  play() {
    let offset = this.pausedAt;
    // 如果已经播放完成,重新播放
    if (offset >= this.getDuration()) {
      offset = 0;
    }
    this.sourceNode = this.context.createBufferSource();
    this.sourceNode.connect(this.context.destination);
    this.sourceNode.buffer = this.buffer;
    this.sourceNode.start(0, offset);
    this.startedAt = this.context.currentTime - offset;
    this.pausedAt = 0;
    this.playing = true;
    if (typeof (this.callback) === 'function') {
      // console.log('------------callback: play');
      this.callback('play', this);
    }
    this.update();
  }

  update() {
    window.requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame;
    if (this.getCurrentTime() < this.getDuration()) {
      window.requestAnimationFrame(this.update);
    } else {
      this.stop();
      if (typeof (this.callback) === 'function') {
        // console.log('------------callback: stop');
        this.callback('stop', this);
      }
    }
  }
  // 暂停
  pause() {
    const elapsed = this.context.currentTime - this.startedAt;
    this.stop();
    this.pausedAt = elapsed;
    if (typeof (this.callback) === 'function') {
       // console.log('------------callback: pause');
      this.callback('pause', this);
    }
  }
  // 停止
  stop() {
    if (this.sourceNode) {
      this.sourceNode.disconnect();
      this.sourceNode.stop(0);
      this.sourceNode = null;
    }
    this.pausedAt = 0;
    this.startedAt = 0;
    this.playing = false;
    if (typeof (this.callback) === 'function') {
       // console.log('------------callback: stop');
      this.callback('stop', this);
    }
  }
  // 已播放时间
  getCurrentTime() {
    if (this.pausedAt) {
      return this.pausedAt;
    }
    if (this.startedAt) {
      return this.context.currentTime - this.startedAt;
    }
    return 0;
  }
  // 总时长
  getDuration() {
    return this.buffer.duration;
  }
  // 事件绑定
  bind(buffer) {
    this.buffer = buffer;
    if (this.stopBtn) {
      this.stopBtn.addEventListener('click', () => {
        this.stop();
      });
    }
    this.playBtn.addEventListener('click', () => {
      if (this.playing) {
        this.pause();
      } else {
        this.play();
      }
    });

    // function update() {
    //   window.requestAnimationFrame(update);
    // //   info.innerHTML = sound.getCurrentTime().toFixed(1) + '/' + sound.getDuration().toFixed(1);
    // }
    // update();
  }

  init() {
    window._audioContext = window._audioContext || new (window.AudioContext || window.webkitAudioContext)();
    this.context = window._audioContext;
    const request = new XMLHttpRequest();
    request.open('GET', this.url, true);
    request.responseType = 'arraybuffer';
    request.addEventListener('load', () => {
      this.context.decodeAudioData(
        request.response,
        (buffer) => {
          this.bind(buffer);
        },
        () => {
        });
    });
    request.send();
  }
}

export default AudioBuffer;

使用:

  componentDidMount() {
    const audio = this.refs.videoPlay
    const url = audio.getAttribute('data-url')
    if(audio && url ){  
        this.audio = new AudioBuffer({'url':url,'playBtn':this.refs.videoPlay})
      }
    } 
  }