UNPKG

@babylonjs/viewer

Version:

The Babylon Viewer aims to simplify a specific but common Babylon.js use case: loading, viewing, and interacting with a 3D model.

530 lines (525 loc) 20.1 kB
import { O as Observable, aY as Tools, bI as WebRequest, y as Logger } from './index-MZPybX0H.esm.js'; import { A as AbstractSound, _ as _AbstractSoundInstance } from './abstractSoundInstance-LF0gFVmD.esm.js'; import { _ as _HasSpatialAudioOptions, a as _StereoAudio, b as _SpatialWebAudio, c as _WebAudioBusAndSoundSubGraph } from './spatialWebAudio-BHkDY-w9.esm.js'; import { _ as _GetUrlForStreaming, a as _CleanUrl } from './spatialWebAudioUpdaterComponent-jS7MKp73.esm.js'; import './abstractSoundSource-5MTb28Na.esm.js'; import './webAudioBaseSubGraph-BjGMAHX6.esm.js'; /** * Abstract class representing a streaming sound. * * A streaming sound has a sound buffer that is loaded into memory in chunks as it is played. This allows it to be played * more quickly than a static sound, but it also means that it cannot have loop points or playback rate changes. * * Due to the way streaming sounds are typically implemented, there can be a significant delay when attempting to play * a streaming sound for the first time. To prevent this delay, it is recommended to preload instances of the sound * using the {@link IStreamingSoundStoredOptions.preloadCount} options, or the {@link preloadInstanceAsync} and * {@link preloadInstancesAsync} methods before calling the `play` method. * * Streaming sounds are created by the {@link CreateStreamingSoundAsync} function. */ class StreamingSound extends AbstractSound { constructor(name, engine, options) { super(name, engine, options); this._preloadedInstances = new Array(); } /** * The number of instances to preload. Defaults to `1`. */ get preloadCount() { return this._options.preloadCount ?? 1; } /** * Returns the number of instances that have been preloaded. */ get preloadCompletedCount() { return this._preloadedInstances.length; } /** * Preloads an instance of the sound. * @returns A promise that resolves when the instance is preloaded. */ // eslint-disable-next-line @typescript-eslint/promise-function-async, no-restricted-syntax preloadInstanceAsync() { const instance = this._createInstance(); this._addPreloadedInstance(instance); return instance.preloadedPromise; } /** * Preloads the given number of instances of the sound. * @param count - The number of instances to preload. * @returns A promise that resolves when all instances are preloaded. */ async preloadInstancesAsync(count) { for (let i = 0; i < count; i++) { // eslint-disable-next-line @typescript-eslint/no-floating-promises this.preloadInstanceAsync(); } await Promise.all(this._preloadedInstances.map(async (instance) => await instance.preloadedPromise)); } /** * Plays the sound. * - Triggers `onEndedObservable` if played for the full duration and the `loop` option is not set. * @param options The options to use when playing the sound. Options set here override the sound's options. */ play(options = {}) { if (this.state === 5 /* SoundState.Paused */) { this.resume(options); return; } let instance; if (this.preloadCompletedCount > 0) { instance = this._preloadedInstances[0]; instance.startOffset = this.startOffset; this._removePreloadedInstance(instance); } else { instance = this._createInstance(); } const onInstanceStateChanged = () => { if (instance.state === 3 /* SoundState.Started */) { this._stopExcessInstances(); instance.onStateChangedObservable.removeCallback(onInstanceStateChanged); } }; instance.onStateChangedObservable.add(onInstanceStateChanged); options.startOffset ??= this.startOffset; options.loop ??= this.loop; options.volume ??= 1; this._beforePlay(instance); instance.play(options); this._afterPlay(instance); } /** * Resumes the sound. * - Only options explicitly set here override the paused instance's current options; unset options keep their paused values so playback continues from where it was paused. * @param options The options to use when resuming the sound. */ resume(options) { super.resume(options); } /** * Stops the sound. */ stop() { this._setState(1 /* SoundState.Stopped */); if (!this._instances) { return; } for (const instance of Array.from(this._instances)) { instance.stop(); } } _addPreloadedInstance(instance) { if (!this._preloadedInstances.includes(instance)) { this._preloadedInstances.push(instance); } } _removePreloadedInstance(instance) { const index = this._preloadedInstances.indexOf(instance); if (index !== -1) { this._preloadedInstances.splice(index, 1); } } } /** @internal */ class _StreamingSoundInstance extends _AbstractSoundInstance { constructor(sound) { super(sound); /** @internal */ this.onReadyObservable = new Observable(); /** @internal */ this.preloadedPromise = new Promise((resolve, reject) => { this._rejectPreloadedPromise = reject; this._resolvePreloadedPromise = resolve; }); this.onErrorObservable.add(this._rejectPreloadedPromise); this.onReadyObservable.add(this._resolvePreloadedPromise); } /** @internal */ set startOffset(value) { this._options.startOffset = value; } /** @internal */ dispose() { super.dispose(); this.onErrorObservable.clear(); this.onReadyObservable.clear(); this._resolvePreloadedPromise(); } } /** @internal */ class _WebAudioStreamingSound extends StreamingSound { /** @internal */ constructor(name, engine, options) { super(name, engine, options); this._stereo = null; this._options = { autoplay: options.autoplay ?? false, loop: options.loop ?? false, maxInstances: options.maxInstances ?? Infinity, preloadCount: options.preloadCount ?? 1, startOffset: options.startOffset ?? 0, }; this._subGraph = new _WebAudioStreamingSound._SubGraph(this); } /** @internal */ async _initAsync(source, options) { const audioContext = this.engine._audioContext; if (!(audioContext instanceof AudioContext)) { throw new Error("Unsupported audio context type."); } this._audioContext = audioContext; this._source = source; if (options.outBus) { this.outBus = options.outBus; } else if (options.outBusAutoDefault !== false) { await this.engine.isReadyPromise; this.outBus = this.engine.defaultMainBus; } await this._subGraph.initAsync(options); if (_HasSpatialAudioOptions(options)) { this._initSpatialProperty(); } if (this.preloadCount) { await this.preloadInstancesAsync(this.preloadCount); } if (options.autoplay) { this.play(options); } this.engine._addSound(this); } /** @internal */ get _inNode() { return this._subGraph._inNode; } /** @internal */ get _outNode() { return this._subGraph._outNode; } /** @internal */ get stereo() { return this._stereo ?? (this._stereo = new _StereoAudio(this._subGraph)); } /** @internal */ dispose() { super.dispose(); this._stereo = null; this._subGraph.dispose(); this.engine._removeSound(this); } /** @internal */ getClassName() { return "_WebAudioStreamingSound"; } _createInstance() { return new _WebAudioStreamingSoundInstance(this, this._options); } _connect(node) { const connected = super._connect(node); if (!connected) { return false; } // If the wrapped node is not available now, it will be connected later by the subgraph. if (node._inNode) { this._outNode?.connect(node._inNode); } return true; } _disconnect(node) { const disconnected = super._disconnect(node); if (!disconnected) { return false; } if (node._inNode) { this._outNode?.disconnect(node._inNode); } return true; } _createSpatialProperty(autoUpdate, minUpdateTime) { return new _SpatialWebAudio(this._subGraph, autoUpdate, minUpdateTime); } _getOptions() { return this._options; } } _WebAudioStreamingSound._SubGraph = class extends _WebAudioBusAndSoundSubGraph { get _downstreamNodes() { return this._owner._downstreamNodes ?? null; } get _upstreamNodes() { return this._owner._upstreamNodes ?? null; } }; /** @internal */ class _WebAudioStreamingSoundInstance extends _StreamingSoundInstance { constructor(sound, options) { super(sound); this._currentTimeChangedWhilePaused = false; this._enginePlayTime = Infinity; this._enginePauseTime = 0; this._isReady = false; this._isReadyPromise = new Promise((resolve, reject) => { this._resolveIsReadyPromise = resolve; this._rejectIsReadyPromise = reject; }); this._onCanPlayThrough = () => { this._isReady = true; this._resolveIsReadyPromise(this._mediaElement); this.onReadyObservable.notifyObservers(this); }; this._onEnded = () => { this._setState(1 /* SoundState.Stopped */); }; this._onError = (reason) => { this._setState(4 /* SoundState.FailedToStart */); this.onErrorObservable.notifyObservers(reason); this._rejectIsReadyPromise(reason); this.dispose(); }; this._onEngineStateChanged = () => { if (this.engine.state !== "running") { return; } if (this._options.loop && this.state === 2 /* SoundState.Starting */) { this.play(); } this.engine.stateChangedObservable.removeCallback(this._onEngineStateChanged); }; this._onUserGesture = () => { this.play(); }; this._options = options; this._volumeNode = new GainNode(sound._audioContext); if (typeof sound._source === "string") { this._initFromUrl(sound._source); } else if (Array.isArray(sound._source)) { this._initFromUrls(sound._source); } else if (sound._source instanceof HTMLMediaElement) { this._initFromMediaElement(sound._source); } else { throw new Error(`Invalid streaming sound source (${sound._source}).`); } } /** @internal */ get currentTime() { if (this._state === 1 /* SoundState.Stopped */) { return 0; } const timeSinceLastStart = this._state === 5 /* SoundState.Paused */ ? 0 : this.engine.currentTime - this._enginePlayTime; return this._enginePauseTime + timeSinceLastStart + this._options.startOffset; } set currentTime(value) { const restart = this._state === 2 /* SoundState.Starting */ || this._state === 3 /* SoundState.Started */; if (restart) { this._mediaElement.pause(); this._state = 1 /* SoundState.Stopped */; } // Seeking sets an absolute position that is fully described by `startOffset`, so any playback time accumulated // across previous pause/resume cycles must be cleared. Otherwise the stale value leaks into the `currentTime` // getter and the next `pause()` calculation, offsetting them by the total paused duration. this._enginePauseTime = 0; this._options.startOffset = value; if (restart) { this.play({ startOffset: value }); } else if (this._state === 5 /* SoundState.Paused */) { this._currentTimeChangedWhilePaused = true; } } get _outNode() { return this._volumeNode; } /** @internal */ set loop(value) { this._options.loop = value; this._mediaElement.loop = value; } /** @internal */ get startTime() { if (this._state === 1 /* SoundState.Stopped */) { return 0; } return this._enginePlayTime; } /** @internal */ dispose() { super.dispose(); this.stop(); this._sourceNode?.disconnect(this._volumeNode); this._sourceNode = null; this._mediaElement.removeEventListener("error", this._onError); this._mediaElement.removeEventListener("ended", this._onEnded); this._mediaElement.removeEventListener("canplaythrough", this._onCanPlayThrough); for (const source of Array.from(this._mediaElement.children)) { this._mediaElement.removeChild(source); } this.engine.stateChangedObservable.removeCallback(this._onEngineStateChanged); this.engine.userGestureObservable.removeCallback(this._onUserGesture); } /** @internal */ play(options = {}) { if (this._state === 3 /* SoundState.Started */) { return; } if (options.loop !== undefined) { this._options.loop = options.loop; } this._mediaElement.loop = this._options.loop; let startOffset = options.startOffset; if (this._currentTimeChangedWhilePaused) { startOffset = this._options.startOffset; this._currentTimeChangedWhilePaused = false; } else if (this._state === 5 /* SoundState.Paused */) { startOffset = this.currentTime; } if (startOffset && startOffset > 0) { this._mediaElement.currentTime = startOffset; } // Only override the volume when explicitly provided so resuming a paused instance keeps its current volume. if (options.volume !== undefined) { this._volumeNode.gain.value = options.volume; } this._play(); } /** @internal */ pause() { if (this._state !== 2 /* SoundState.Starting */ && this._state !== 3 /* SoundState.Started */) { return; } this._setState(5 /* SoundState.Paused */); this._enginePauseTime += this.engine.currentTime - this._enginePlayTime; this._mediaElement.pause(); } /** @internal */ resume(options = {}) { if (this._state === 5 /* SoundState.Paused */) { this.play(options); } else if (this._currentTimeChangedWhilePaused) { this.play(options); } } /** @internal */ stop() { if (this._state === 1 /* SoundState.Stopped */) { return; } this._stop(); } /** @internal */ getClassName() { return "_WebAudioStreamingSoundInstance"; } _connect(node) { const connected = super._connect(node); if (!connected) { return false; } // If the wrapped node is not available now, it will be connected later by the sound's subgraph. if (node instanceof _WebAudioStreamingSound && node._inNode) { this._outNode?.connect(node._inNode); } return true; } _disconnect(node) { const disconnected = super._disconnect(node); if (!disconnected) { return false; } if (node instanceof _WebAudioStreamingSound && node._inNode) { this._outNode?.disconnect(node._inNode); } return true; } _initFromMediaElement(mediaElement) { Tools.SetCorsBehavior(mediaElement.currentSrc, mediaElement); mediaElement.controls = false; mediaElement.loop = this._options.loop; mediaElement.preload = "auto"; mediaElement.addEventListener("canplaythrough", this._onCanPlayThrough, { once: true }); mediaElement.addEventListener("ended", this._onEnded, { once: true }); mediaElement.addEventListener("error", this._onError, { once: true }); mediaElement.load(); this._sourceNode = new MediaElementAudioSourceNode(this._sound._audioContext, { mediaElement: mediaElement }); this._sourceNode.connect(this._volumeNode); if (!this._connect(this._sound)) { throw new Error("Connect failed"); } this._mediaElement = mediaElement; } _initFromUrl(url) { // Apply any WebRequest URL modifiers (but do NOT download the content — the <audio> element streams natively). // Custom headers cannot be forwarded to HTMLMediaElement; _GetUrlForStreaming logs a warning if any are present. const resolvedUrl = WebRequest.IsCustomRequestAvailable ? _GetUrlForStreaming(_CleanUrl(url)) : _CleanUrl(url); const audio = new Audio(resolvedUrl); this._initFromMediaElement(audio); } _initFromUrls(urls) { const audio = new Audio(); // Apply any WebRequest URL modifiers to each candidate URL (but do NOT download the content — // the <audio> element picks the first compatible format and streams natively). // Custom headers cannot be forwarded to HTMLMediaElement; _GetUrlForStreaming logs a warning if any are present. for (const url of urls) { const source = document.createElement("source"); source.src = WebRequest.IsCustomRequestAvailable ? _GetUrlForStreaming(_CleanUrl(url)) : _CleanUrl(url); audio.appendChild(source); } this._initFromMediaElement(audio); } _play() { this._setState(2 /* SoundState.Starting */); if (!this._isReady) { this._playWhenReady(); return; } if (this._state !== 2 /* SoundState.Starting */) { return; } if (this.engine.state === "running") { const result = this._mediaElement.play(); this._enginePlayTime = this.engine.currentTime; this._setState(3 /* SoundState.Started */); // It's possible that the play() method fails on Safari, even if the audio engine's state is "running". // This occurs when the audio context is paused by the system and resumed automatically by the audio engine // without a user interaction (e.g. when the Vision Pro exits and reenters immersive mode). // eslint-disable-next-line github/no-then result.catch(() => { this._setState(4 /* SoundState.FailedToStart */); if (this._options.loop) { this.engine.userGestureObservable.addOnce(this._onUserGesture); } }); } else if (this._options.loop) { this.engine.stateChangedObservable.add(this._onEngineStateChanged); } else { this.stop(); this._setState(4 /* SoundState.FailedToStart */); } } _playWhenReady() { this._isReadyPromise // eslint-disable-next-line github/no-then .then(() => { this._play(); }) // eslint-disable-next-line github/no-then .catch(() => { Logger.Error("Streaming sound instance failed to play"); this._setState(4 /* SoundState.FailedToStart */); }); } _stop() { this._mediaElement.pause(); this._onEnded(); this.engine.stateChangedObservable.removeCallback(this._onEngineStateChanged); } } export { _WebAudioStreamingSound }; //# sourceMappingURL=webAudioStreamingSound-DVbUdywu.esm.js.map