Html5 enabled for speed rates on HowlerJs breaks in Safari

74 views Asked by At

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();
0

There are 0 answers