Bin
2025-12-16 9e0b2ba2c317b1a86212f24cbae3195ad1f3dbfa
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
import type { WaveformAudio } from "../Media/WaveformAudio";
import { Player } from "./Player";
import { ff } from "@humansignal/core";
 
export class Html5Player extends Player {
  mute() {
    super.mute();
    if (this.audio?.el) {
      this.audio.el.muted = true;
    }
  }
 
  unmute() {
    super.unmute();
    if (this.audio?.el) {
      this.audio.el.muted = false;
    }
  }
 
  /**
   * Get current playback speed
   */
  get rate() {
    if (this.audio?.el) {
      if (this.audio.el.playbackRate !== this._rate) {
        this.audio.el.playbackRate = this._rate; // restore the correct rate
      }
    }
 
    return this._rate;
  }
 
  /**
   * Set playback speed
   */
  set rate(value: number) {
    const rateChanged = this._rate !== value;
 
    this._rate = value;
 
    if (rateChanged) {
      if (this.audio?.el) {
        this.audio.el.playbackRate = value;
      }
      this.wf.invoke("rateChanged", [value]);
    }
  }
 
  init(audio: WaveformAudio) {
    super.init(audio);
 
    if (!this.audio || !this.audio.el) return;
 
    this.audio.on("resetSource", this.handleResetSource);
 
    this.audio.el.addEventListener("play", this.handlePlayed);
    this.audio.el.addEventListener("pause", this.handlePaused);
  }
 
  destroy() {
    super.destroy();
 
    if (this.audio?.el) {
      this.audio.el.removeEventListener("play", this.handlePlayed);
      this.audio.el.removeEventListener("pause", this.handlePaused);
    }
  }
 
  protected adjustVolume(): void {
    if (this.audio?.el) {
      this.audio.el.volume = this.volume;
    }
  }
 
  protected playAudio(_start?: number, _duration?: number): void {
    if (!this.audio || !this.audio.el) return;
 
    this.audio.el.currentTime = this.currentTime;
    this.audio.el.addEventListener("ended", this.handleEnded);
    this.bufferPromise = new Promise((resolve) => {
      this.bufferResolve = resolve;
    });
 
    const time = this.currentTime;
 
    Promise.all([this.audio.el.play(), this.bufferPromise]).then(() => {
      this.timestamp = performance.now();
 
      // We need to compensate for the time it took to load the buffer
      // otherwise the audio will be out of sync of the timer we use to
      // render updates
      if (this.audio?.el) {
        // This must not be notifying of this adjustment otherwise it can cause sync issues and near infinite loops
        this.setCurrentTime(time);
        this.audio.el.currentTime = this.currentTime;
        this.watch();
      }
    });
  }
 
  protected updateCurrentSourceTime(timeChanged: boolean) {
    if (timeChanged && this.audio?.el) {
      this.audio.el.currentTime = this.time;
    }
  }
 
  protected canPause() {
    return !!(this.audio?.el && !this.audio.el.paused && this.hasPlayed);
  }
 
  protected disconnectSource(): boolean {
    if (super.disconnectSource()) {
      this.audio?.el?.removeEventListener("ended", this.handleEnded);
      return true;
    }
    return false;
  }
 
  protected handleResetSource = async () => {
    if (!this.audio?.el) return;
 
    const wasPlaying = this.playing;
 
    this.stop();
    // We don't need to load the audio when the feature flag is active
    if (!ff.isActive(ff.FF_SYNCED_BUFFERING)) this.audio.el.load();
 
    if (wasPlaying) this.play();
  };
}