babylon-mmd
Version:
babylon.js mmd loader and runtime
530 lines (529 loc) • 16.6 kB
JavaScript
import { Observable } from "@babylonjs/core/Misc/observable";
/**
* This class is used to pooling the audio element
*/
export class AudioElementPool {
_audioElements = [];
/**
* Rent the audio element
* @returns The audio element
*/
rent() {
if (this._audioElements.length === 0) {
return new Audio();
}
else {
const audio = this._audioElements.pop();
audio.loop = false;
audio.autoplay = false;
audio.playbackRate = 1;
audio.volume = 1;
audio.muted = false;
audio.preservesPitch = true;
audio.ondurationchange = null;
audio.onerror = null;
audio.onplaying = null;
audio.onpause = null;
audio.onseeked = null;
return audio;
}
}
/**
* Return the audio element
*
* Returned audio element should not be used anymore
* @param audioElement The audio element
*/
return(audioElement) {
audioElement.pause();
audioElement.src = "";
audioElement.load();
this._audioElements.push(audioElement);
}
}
/**
* Stream audio player
*
* This class is used to play the audio from the stream
*
* It is suitable for playing long sounds because it plays even if all of the audio is not loaded
*
* Wrapper of `HTMLAudioElement` which handles audio playback permission issues gracefully
*/
export class StreamAudioPlayer {
/**
* Default global audio element pool instance
*/
static DefaultAudioElementPool = new AudioElementPool();
/**
* On load error observable
*
* This observable is notified when the audio load is failed
*/
onLoadErrorObservable;
/**
* On duration changed observable
*
* This observable is notified when the audio duration is changed
*/
onDurationChangedObservable;
/**
* On mute state changed observable
*
* This observable is notified when the mute state is changed
*/
onPlaybackRateChangedObservable;
/**
* On mute state changed observable
*
* This observable is notified when the mute state is changed
*/
onMuteStateChangedObservable;
/**
* On play observable
*
* This observable is notified when the player is played
*/
onPlayObservable;
/**
* On pause observable
*
* This observable is notified when the player is paused
*/
onPauseObservable;
/**
* On seek observable
*
* This observable is notified when the player is seeked
*/
onSeekObservable;
_pool;
_audio;
_duration;
_playbackRate;
_isVirtualPlay;
_virtualStartTime;
_virtualPaused;
_virtualPauseCurrentTime;
_metadataLoaded;
_bindedDispose;
_disposeObservableObject;
/**
* Create a stream audio player
*
* In general disposeObservable should be `Scene` of Babylon.js
*
* @param disposeObservable Objects that limit the lifetime of this instance
* @param options Options
*/
constructor(disposeObservable, options = {}) {
const poolOption = options.pool ?? false;
this.onLoadErrorObservable = new Observable();
this.onDurationChangedObservable = new Observable();
this.onPlaybackRateChangedObservable = new Observable();
this.onMuteStateChangedObservable = new Observable();
this.onPlayObservable = new Observable();
this.onPauseObservable = new Observable();
this.onSeekObservable = new Observable();
const pool = this._pool = typeof poolOption === "boolean"
? poolOption
? StreamAudioPlayer.DefaultAudioElementPool
: null
: poolOption;
const audio = this._audio = pool !== null
? pool.rent()
: new Audio();
audio.loop = false;
audio.autoplay = false;
this._duration = 0;
this._playbackRate = 1;
this._isVirtualPlay = false;
this._virtualStartTime = 0;
this._virtualPaused = true;
this._virtualPauseCurrentTime = 0;
this._metadataLoaded = false;
audio.ondurationchange = this._onDurationChanged;
audio.onerror = this._onLoadError;
audio.onplaying = this._onPlay;
audio.onpause = this._onPause;
audio.onseeked = this._onSeek;
this._bindedDispose = this.dispose.bind(this);
this._disposeObservableObject = disposeObservable;
if (this._disposeObservableObject !== null) {
this._disposeObservableObject.onDisposeObservable.add(this._bindedDispose);
}
}
_onDurationChanged = () => {
this._duration = this._audio.duration;
if (this._isVirtualPlay) {
this._isVirtualPlay = false;
this.onMuteStateChangedObservable.notifyObservers();
}
this._virtualPaused = true;
this._virtualPauseCurrentTime = 0;
this._metadataLoaded = true;
this.onDurationChangedObservable.notifyObservers();
};
_onLoadError = () => {
this._duration = 0;
if (this._isVirtualPlay) {
this._isVirtualPlay = false;
this.onMuteStateChangedObservable.notifyObservers();
}
this._virtualPaused = true;
this._virtualPauseCurrentTime = 0;
this._metadataLoaded = false;
this.onLoadErrorObservable.notifyObservers();
this.onDurationChangedObservable.notifyObservers();
};
_onPlay = () => {
if (!this._isVirtualPlay) {
this._audio.playbackRate = this._playbackRate;
}
this.onPlayObservable.notifyObservers();
};
_onPause = () => {
if (!this._isVirtualPlay) {
this.onPauseObservable.notifyObservers();
}
else {
if (this._virtualPaused) {
this.onPauseObservable.notifyObservers();
}
}
};
_ignoreSeekedEventOnce = false;
_onSeek = () => {
if (this._ignoreSeekedEventOnce) {
this._ignoreSeekedEventOnce = false;
return;
}
this.onSeekObservable.notifyObservers();
};
/**
* Audio duration (in seconds)
*/
get duration() {
return this._duration;
}
/**
* Current time (in seconds)
*
* This property may be slow to update
*/
get currentTime() {
if (this._isVirtualPlay) {
if (this._virtualPaused) {
return this._virtualPauseCurrentTime;
}
else {
const computedTime = (performance.now() / 1000 - this._virtualStartTime) * this._playbackRate;
if (computedTime > this._duration) {
this._virtualPaused = true;
this._virtualPauseCurrentTime = this._duration;
this._onPause();
return this._virtualPauseCurrentTime;
}
else {
return computedTime;
}
}
}
else {
return this._audio?.currentTime ?? 0;
}
}
set currentTime(value) {
if (this._isVirtualPlay) {
if (this._virtualPaused) {
this._virtualPauseCurrentTime = value;
}
else {
this._virtualStartTime = performance.now() / 1000 - value / this._playbackRate;
}
this._onSeek();
}
else {
if (this._audio !== null) {
this._audio.currentTime = value;
}
}
}
/** @internal */
_setCurrentTimeWithoutNotify(value) {
if (this._isVirtualPlay) {
if (this._virtualPaused) {
this._virtualPauseCurrentTime = value;
}
else {
this._virtualStartTime = performance.now() / 1000 - value / this._playbackRate;
}
}
else {
this._ignoreSeekedEventOnce = true;
if (this._audio !== null) {
this._audio.currentTime = value;
}
}
}
/**
* Volume (0.0 to 1.0)
*/
get volume() {
return this._audio?.volume ?? 0;
}
set volume(value) {
if (this._audio !== null) {
this._audio.volume = value;
}
}
/**
* Whether the audio is muted
*/
get muted() {
return this._isVirtualPlay;
}
/**
* Mute the audio
*/
mute() {
if (this._audio === null)
return;
if (this._isVirtualPlay)
return;
this._isVirtualPlay = true;
this._virtualStartTime = performance.now() / 1000 - this._audio.currentTime / this._playbackRate;
this._virtualPaused = this._audio.paused;
this._virtualPauseCurrentTime = this._audio.currentTime;
this._audio.pause();
this.onMuteStateChangedObservable.notifyObservers();
}
/**
* Unmute the audio
*
* Unmute is possible failed if user interaction is not performed
* @returns Whether the audio is unmuted
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
async unmute() {
if (this._audio === null)
return false;
if (!this._isVirtualPlay)
return true;
let notAllowedError = false;
this._ignoreSeekedEventOnce = true;
if (this._virtualPaused) {
this._audio.currentTime = this._virtualPauseCurrentTime;
}
else {
this._audio.currentTime = (performance.now() / 1000 - this._virtualStartTime) * this._playbackRate;
try {
await this._audio.play();
this._audio.playbackRate = this._playbackRate;
}
catch (e) {
if (!(e instanceof DOMException && e.name === "NotAllowedError"))
throw e;
notAllowedError = true;
}
}
if (!notAllowedError) {
this._isVirtualPlay = false;
this._virtualPaused = true;
this._virtualPauseCurrentTime = 0;
this.onMuteStateChangedObservable.notifyObservers();
return true;
}
return false;
}
/**
* Playback rate (0.07 to 16.0)
*/
get playbackRate() {
return this._playbackRate;
}
set playbackRate(value) {
this._setPlaybackRateWithoutNotify(value);
this.onPlaybackRateChangedObservable.notifyObservers();
}
/** @internal */
_setPlaybackRateWithoutNotify(value) {
if (this._isVirtualPlay && !this._virtualPaused) {
const nowInSec = performance.now() / 1000;
const currentTime = (nowInSec - this._virtualStartTime) * this._playbackRate;
this._virtualStartTime = nowInSec - currentTime / value;
}
this._playbackRate = value;
if (this._audio !== null) {
this._audio.playbackRate = value;
}
}
/**
* Determines whether or not the browser should adjust the pitch of the audio to compensate for changes to the playback rate made by setting
*/
get preservesPitch() {
return this._audio?.preservesPitch ?? true;
}
set preservesPitch(value) {
if (this._audio !== null) {
this._audio.preservesPitch = value;
}
}
/**
* Whether the player is paused
*/
get paused() {
if (this._isVirtualPlay) {
return this._virtualPaused;
}
else {
return this._audio?.paused ?? true;
}
}
/**
* Audio source URL
*/
get source() {
return this._audio?.src ?? "";
}
set source(value) {
if (this._audio === null) {
return;
}
if (value === this._audio.src)
return;
this._audio.src = value;
this._metadataLoaded = false;
if (this._isVirtualPlay) {
this._isVirtualPlay = false;
this.onMuteStateChangedObservable.notifyObservers();
}
this._virtualPaused = true;
this._virtualPauseCurrentTime = 0;
this._audio.load();
}
/**
* Whether the audio metadata(durations) is loaded
*/
get metadataLoaded() {
return this._metadataLoaded;
}
async _virtualPlayAsync() {
if (this._metadataLoaded) {
if (this._virtualPaused) {
this._virtualStartTime = performance.now() / 1000 - this._virtualPauseCurrentTime / this._playbackRate;
this._virtualPaused = false;
}
if (!this._isVirtualPlay) {
this._isVirtualPlay = true;
this.onMuteStateChangedObservable.notifyObservers();
}
this._onPlay();
}
else {
await new Promise((resolve, reject) => {
const onDurationChanged = () => {
if (this._virtualPaused) {
this._virtualStartTime = performance.now() / 1000 - this._virtualPauseCurrentTime / this._playbackRate;
this._virtualPaused = false;
}
if (!this._isVirtualPlay) {
this._isVirtualPlay = true;
this.onMuteStateChangedObservable.notifyObservers();
}
this._onPlay();
this.onLoadErrorObservable.removeCallback(onLoadError);
resolve();
};
const onLoadError = () => {
this.onDurationChangedObservable.removeCallback(onDurationChanged);
reject(new DOMException("The media resource indicated by the src attribute or assigned media provider object was not suitable.", "NotSupportedError"));
};
this.onDurationChangedObservable.addOnce(onDurationChanged);
this.onLoadErrorObservable.addOnce(onLoadError);
});
}
}
_playRequestBlocking = false;
/**
* Play the audio from the current position
*
* If context don't have permission to play the audio, play audio in a mute state
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
async play() {
if (this._isVirtualPlay && !this._virtualPaused)
return;
if (this._isVirtualPlay) {
await this._virtualPlayAsync();
return;
}
if (this._playRequestBlocking)
return;
this._playRequestBlocking = true;
try {
await this._audio?.play();
}
catch (e) {
if (e instanceof DOMException && e.name === "NotAllowedError") {
await this._virtualPlayAsync();
}
else {
throw e;
}
}
finally {
this._playRequestBlocking = false;
}
}
/**
* Pause the audio
*/
pause() {
if (this._isVirtualPlay) {
if (this._virtualPaused)
return;
this._virtualPaused = true;
this._virtualPauseCurrentTime = (performance.now() / 1000 - this._virtualStartTime) * this._playbackRate;
this._onPause();
}
else {
this._audio?.pause();
}
}
/**
* Dispose the player
*/
dispose() {
if (this._audio === null) {
return;
}
const audio = this._audio;
audio.pause();
audio.ondurationchange = null;
audio.onerror = null;
audio.onplaying = null;
audio.onpause = null;
audio.onseeked = null;
audio.src = "";
audio.load();
if (this._pool !== null) {
this._pool.return(audio);
}
else {
this._audio.remove();
}
this.onLoadErrorObservable.clear();
this.onDurationChangedObservable.clear();
this.onPlaybackRateChangedObservable.clear();
this.onMuteStateChangedObservable.clear();
this.onPlayObservable.clear();
this.onPauseObservable.clear();
this.onSeekObservable.clear();
if (this._disposeObservableObject !== null) {
this._disposeObservableObject.onDisposeObservable.removeCallback(this._bindedDispose);
}
this._audio = null;
this._pool = null;
}
}