WXInlinePlayer的音频播放部分。

入口很简单,判断是不是微信环境。

1
2
3
4
5
6
7
8
9
import Util from '../util/util';
import BrowserSound from './browser';
import WeChatSound from './wechat';

function Sound(opt) {
  return Util.isWeChat() ? new WeChatSound(opt) : new BrowserSound(opt);
}

export default Sound;

由于微信部分和浏览器部分区别不大,属于细节性的区别,所以下面从浏览器角度看一下代码。先来看数据流入的地方:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
  decode(data) {
    if (data.length) {
      data = Buffer.from(data);
      this.data = Buffer.concat([this.data, data]); // 将数据放到this.data中保存起来
      if (this.context) {
        return new Promise(resolve => {
          this.context.decodeAudioData(       // 调用AudioContext.decodeAudioData()
            this.data.buffer,
            buffer => {
              this._onDecodeSuccess(buffer);
              resolve();
            },
            error => {
              this._onDecodeError(error);
              resolve();
            }
          );
        });
      }
    }
    return Promise.resolve();
  }

这个函数是在processor收到解码器decode消息后调用的,这时候是解码器解封装了一段数据。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
  _onDecodeSuccess(audioBuffer) {
    const audioSrc = this.context.createBufferSource();
    audioSrc.onended = this._onAudioBufferEnded.bind(this);   // 播放完及时清理this.audioSrcNodes

    if (!this.playStartedAt) {
      const { duration } = audioBuffer;       // 第一段音频数据的时长
      const { currentTime, baseLatency, sampleRate } = this.context;
      const startDelay = duration + (baseLatency || 128 / sampleRate); // 第一段数据延迟一定时间再播放
      this.playStartedAt = currentTime + startDelay;                   // 记录开始播放的时间
    }

    audioSrc.buffer = audioBuffer;
    if (this.state == 'running') {
      try {
        audioSrc.connect(this.gainNode);
        audioSrc.start(this.totalTimeScheduled + this.playStartedAt); // 通过时间轴,指定每段音频的播放开始时间,避免卡顿
      } catch (e) {}
    }

    this.audioSrcNodes.push({
      source: audioSrc,
      duration: audioBuffer.duration,         // 这段音频的时长
      timestamp: this.totalTimeScheduled      // 这段音频在所有可播放的音频段中的时间位置
    });

    this.totalTimeScheduled += audioBuffer.duration;
    this.duration += audioBuffer.duration;

    this.data = Buffer.alloc(0);
    this.emit('decode:success');
  }

  _onDecodeError(e) {
    this.emit('decode:error', e);       // 音频解码失败返回事件
  }

除了上面这些部分,还有一个unblock函数值得注意,这个函数在代码中几乎相当于音频播放的start,没有其它用处。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
  unblock(offset) {
    if (this.state != 'blocked') {
      return;
    }

    this.state = 'running';
    this.resume();
    this.setBlockedCurrTime(offset);

    this.playStartedAt = 0;     // 初始化时间变量状态
    this.totalTimeScheduled = 0;
    for (let i = 0; i < this.audioSrcNodes.length; i++) {
      const { source, timestamp, duration } = this.audioSrcNodes[i];
      source.onended = null;
      source.disconnect();

      const audioSrc = this.context.createBufferSource();
      audioSrc.onended = this._onAudioBufferEnded.bind(this);   // 清理播放完的this.audioSrcNodes
      if (!this.playStartedAt) {
        const { currentTime, baseLatency, sampleRate } = this.context;
        const startDelay = duration + (baseLatency || 128 / sampleRate);
        this.playStartedAt = currentTime + startDelay;
      }

      audioSrc.buffer = source.buffer;
      try {
        audioSrc.connect(this.gainNode);
        audioSrc.start(
          this.totalTimeScheduled + this.playStartedAt,   // 给音频安排schedule,让它在适当的时间播放
          !i ? offset / 1000 - timestamp : 0
        );
      } catch (e) {}

      this.audioSrcNodes[i].source = audioSrc;
      this.audioSrcNodes[i].timestamp = this.totalTimeScheduled;
      this.totalTimeScheduled += duration;
    }
  }