I have an audio component in Vue3 which uses HowlerJs to manage the audio functions. I need to have speed increase buttons. So I added html5: true to the Howler object. This works great in Chrome, but stops working in Safari.
I made a singleton class to handle one object at a time, which removed the Audio pool exhausted error. With this change still not even 1.0 plays then in safari.
Note that 1.0 speeds work fine on Safari without html5: true.
Here is the code:
const props = defineProps<{ audioUrl: string, currentSound?: number, stopPlaying?: boolean }>();
const isPlaying = ref(false);
const bigNumber = 9999;
const max = ref(bigNumber);
const current = ref(0);
const isLoading = ref(true);
const router = useRouter();
router.beforeEach(() => {
sound?.stop();
});
const buttonsActive: Record<string, boolean> = reactive({
100: false,
150: false,
175: false,
});
let interval: NodeJS.Timer;
let id = 0;
let sound: Howl;
const emit = defineEmits(['playingSound']);
const playSound = (): void => {
if (!isPlaying.value) {
isPlaying.value = true;
const duration = audioManager.getSoundDuration(id);
audioManager.makeSound(id);
const player = audioManager.getPlayer(id);
useSoundOnAudio(player, duration);
player.play(id);
emit('playingSound', id.valueOf());
} else {
audioManager.pause(id);
isPlaying.value = false;
}
};
const audioContext = new window.AudioContext();
const getAudioDurantion = async (blob: Blob): Promise<number> => {
const arrayBuffer = await blob.arrayBuffer();
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
return audioBuffer.duration;
};
const initialize = async (): Promise<void> => {
try {
if (!props.audioUrl) { return; }
const { data: audioFile } = await AssessmentService.getInterviewAudio(props.audioUrl);
// set the audio type context
const audioType = translateContentTypeToExtension(audioFile.value as Blob);
if (!audioType) {
console.error('could not find type for audio file');
return;
}
let duration;
const audioUrl = URL.createObjectURL(audioFile.value as Blob);
if (audioFile.value) {
duration = await getAudioDurantion(audioFile.value);
}
id = audioManager.registerPlayer(audioUrl, audioType, duration);
isLoading.value = false;
} catch (error) {
console.error(error);
}
};
const handlePlaybackButtons = (name: number): void => {
for (const b of Object.keys(buttonsActive)) {
if (parseInt(b) === name) {
buttonsActive[b] = true;
continue;
}
buttonsActive[b] = false;
}
const player = audioManager.getPlayer(id);
player.rate(name / 100);
};
const formatSeconds = (seconds: number): string => {
if (seconds < 3600) {
return new Date(seconds * 1000).toISOString().substring(14, 19);
} else {
return new Date(seconds * 1000).toISOString().substring(11, 16);
}
};
const useSoundOnAudio = (soundForPlayer: Howl, duration: number): void => {
soundForPlayer.once('load', () => {
if (duration) {
max.value = Math.floor(duration);
interval = setInterval(() => {
if (audioManager.getPlayer(id).playing(id)) {
current.value++;
}
}, 1000);
}
});
soundForPlayer.on('stop', () => {
current.value = sound.duration(id);
});
soundForPlayer.on('end', () => {
current.value = 0;
isPlaying.value = false;
});
soundForPlayer.on('rate', () => {
clearInterval(interval);
const player = audioManager.getPlayer(id);
interval = setInterval(() => {
if (player.playing(id)) {
current.value++;
}
}, 1000 / player.rate());
});
};
import { Howl } from 'howler';
class AudioManager {
players: Record<number, Howl>;
currentPlayerId: number | null;
sounds: { id: number, audioFile: string, audioType: string, duration: number }[];
constructor() {
this.players = {};
this.currentPlayerId = null;
this.sounds = [];
}
getPlayer(id: number): Howl {
return this.players[id];
}
getSoundDuration(id: number): number {
const f = this.sounds.find(s => s.id === id);
return f?.duration || 0;
}
makeSound(id: number): void {
const playerMeta = this.sounds.find(s => s.id === id);
if (!playerMeta?.audioFile || !playerMeta.audioType) { return undefined; }
const howler = new Howl({
src: [playerMeta.audioFile],
format: [playerMeta.audioType],
html5: true,
});
this.players[id] = howler;
}
registerPlayer(audioFile: string, audioType: string, duration?: number): number {
const randomId = Math.floor(Math.random() * 100);
this.sounds.push({ id: randomId, audioFile, audioType, duration: duration || 0 });
return randomId;
}
unregisterPlayer(id: number): void {
if (this.players[id]) {
this.players[id].stop();
this.players[id].unload();
delete this.players[id];
}
if (this.currentPlayerId === id) {
this.currentPlayerId = null;
}
}
play(id: number): Howl | void {
if (this.currentPlayerId && this.currentPlayerId !== id) {
this.players[this.currentPlayerId].pause();
}
this.currentPlayerId = id;
this.players[id].play();
}
pause(id: number): void {
if (this.currentPlayerId === id) {
this.players[id].pause();
this.currentPlayerId = null;
delete this.players[id];
}
}
}
export const audioManager = new AudioManager();