Add error handling for Howler sound methods (#7853)

* Specifically for "Maximum call stack size exceeded" errors.
This commit is contained in:
ViktorVovk
2025-09-26 11:11:58 +03:00
committed by GitHub
parent b9640f0049
commit 542a841791

View File

@@ -30,6 +30,36 @@ namespace gdjs {
return volume;
};
/**
* Handles errors that occur when calling Howler sound methods.
*
* This function provides special handling for "Maximum call stack size exceeded" errors
* that can occur in Howler.js due to recursive method calls in volume(), seek(), fade(),
* and other sound manipulation methods. Instead of crashing the application, these
* specific errors are logged as warnings to allow the application to continue running.
*
* @param error - The error that occurred during the method call
* @param methodName - The name of the Howler method that caused the error (e.g., 'volume', 'seek')
*
* @throws {Error} Re-throws the original error if it's not a stack overflow error
*
*/
const handleHowlerSoundMethodError = (error: unknown, methodName: string) => {
if (
error instanceof Error &&
error.message &&
typeof error.message === 'string' &&
error.message.startsWith('Maximum call stack size exceeded')
) {
console.warn(
`An error occurred when call method "${methodName}":`,
error
);
} else {
throw error;
}
};
/**
* A thin wrapper around a Howl object with:
* * Handling of callbacks when the sound is not yet loaded.
@@ -103,33 +133,36 @@ namespace gdjs {
* @returns The current instance for chaining.
*/
play(): this {
if (this.isLoaded()) {
const newID = this._howl.play(
this._id === null ? '__default' : this._id
);
this._id = newID;
try {
if (this.isLoaded()) {
const newID = this._howl.play(
this._id === null ? '__default' : this._id
);
this._id = newID;
// Set the howl properties as soon as the sound is played and we have its ID.
this._howl.volume(this._initialVolume, newID); // this._initialVolume is already clamped between 0 and 1.
this._howl.loop(this._loop, newID);
// this._rate is not clamped, but we need to clamp it when passing it to Howler.js as it
// only supports a specific range.
this._howl.rate(gdjs.HowlerSoundManager.clampRate(this._rate), newID);
// Manually handle the play event before we have an ID.
// Before loading, howler won't register events as without an ID we cannot set a listener.
// Once we have an ID, we can transfer control of the events to howler.
// We also need to call them once as Howler doesn't for the first play event.
this._onPlay.forEach((func) => {
// Transfer the event to howler now that we have an ID
this.on('play', func);
func(newID);
});
this._oncePlay.forEach((func) => func(newID));
this._onPlay = [];
this._oncePlay = [];
} else this._howl.once('load', () => this.play()); // Play only once the howl is fully loaded
// Set the howl properties as soon as the sound is played and we have its ID.
this._howl.volume(this._initialVolume, newID); // this._initialVolume is already clamped between 0 and 1.
this._howl.loop(this._loop, newID);
// this._rate is not clamped, but we need to clamp it when passing it to Howler.js as it
// only supports a specific range.
this._howl.rate(gdjs.HowlerSoundManager.clampRate(this._rate), newID);
// Manually handle the play event before we have an ID.
// Before loading, howler won't register events as without an ID we cannot set a listener.
// Once we have an ID, we can transfer control of the events to howler.
// We also need to call them once as Howler doesn't for the first play event.
this._onPlay.forEach((func) => {
// Transfer the event to howler now that we have an ID
this.on('play', func);
func(newID);
});
this._oncePlay.forEach((func) => func(newID));
this._onPlay = [];
this._oncePlay = [];
} else this._howl.once('load', () => this.play()); // Play only once the howl is fully loaded
} catch (error) {
handleHowlerSoundMethodError(error, 'play');
}
return this;
}
@@ -138,7 +171,11 @@ namespace gdjs {
* @returns The current instance for chaining.
*/
pause(): this {
if (this._id !== null) this._howl.pause(this._id);
try {
if (this._id !== null) this._howl.pause(this._id);
} catch (error) {
handleHowlerSoundMethodError(error, 'pause');
}
return this;
}
@@ -147,7 +184,11 @@ namespace gdjs {
* @returns The current instance for chaining.
*/
stop(): this {
if (this._id !== null) this._howl.stop(this._id);
try {
if (this._id !== null) this._howl.stop(this._id);
} catch (error) {
handleHowlerSoundMethodError(error, 'stop');
}
return this;
}
@@ -196,11 +237,15 @@ namespace gdjs {
* @returns The current instance for chaining.
*/
setRate(rate: float): this {
this._rate = rate;
// If the sound has already started playing, then change the value directly.
if (this._id !== null) {
rate = gdjs.HowlerSoundManager.clampRate(rate);
this._howl.rate(rate, this._id);
try {
this._rate = rate;
// If the sound has already started playing, then change the value directly.
if (this._id !== null) {
rate = gdjs.HowlerSoundManager.clampRate(rate);
this._howl.rate(rate, this._id);
}
} catch (error) {
handleHowlerSoundMethodError(error, 'rate');
}
return this;
}
@@ -217,9 +262,13 @@ namespace gdjs {
* @returns The current instance for chaining.
*/
setLoop(loop: boolean): this {
this._loop = loop;
// If the sound has already started playing, then change the value directly.
if (this._id !== null) this._howl.loop(loop, this._id);
try {
this._loop = loop;
// If the sound has already started playing, then change the value directly.
if (this._id !== null) this._howl.loop(loop, this._id);
} catch (error) {
handleHowlerSoundMethodError(error, 'loop');
}
return this;
}
@@ -239,10 +288,14 @@ namespace gdjs {
* @returns The current instance for chaining.
*/
setVolume(volume: float): this {
this._initialVolume = clampVolume(volume);
try {
this._initialVolume = clampVolume(volume);
// If the sound has already started playing, then change the value directly.
if (this._id !== null) this._howl.volume(this._initialVolume, this._id);
// If the sound has already started playing, then change the value directly.
if (this._id !== null) this._howl.volume(this._initialVolume, this._id);
} catch (error) {
handleHowlerSoundMethodError(error, 'volume');
}
return this;
}
@@ -259,7 +312,11 @@ namespace gdjs {
* @returns The current instance for chaining.
*/
setMute(mute: boolean): this {
if (this._id !== null) this._howl.mute(mute, this._id);
try {
if (this._id !== null) this._howl.mute(mute, this._id);
} catch (error) {
handleHowlerSoundMethodError(error, 'mute');
}
return this;
}
@@ -276,7 +333,11 @@ namespace gdjs {
* @returns The current instance for chaining.
*/
setSeek(seek: float): this {
if (this._id !== null) this._howl.seek(seek, this._id);
try {
if (this._id !== null) this._howl.seek(seek, this._id);
} catch (error) {
handleHowlerSoundMethodError(error, 'seek');
}
return this;
}
@@ -302,8 +363,17 @@ namespace gdjs {
* @returns The current instance for chaining.
*/
fade(from: float, to: float, duration: float): this {
if (this._id !== null)
this._howl.fade(clampVolume(from), clampVolume(to), duration, this._id);
try {
if (this._id !== null)
this._howl.fade(
clampVolume(from),
clampVolume(to),
duration,
this._id
);
} catch (error) {
handleHowlerSoundMethodError(error, 'fade');
}
return this;
}