DevGang
Авторизоваться

Аудиоплеер хук для вашего приложения React

В этом посте я расскажу о хуке AudioPlayer для управления упорядоченным воспроизведением аудиофрагментов во всем приложении.

Проблема

В одном из примеров, для сохранения порядка аудиофрагментов я поддерживал классическую структуру данных очереди внутри своего хука.

Вот код для быстрого ознакомления:

import { useState, useEffect } from "react";

const useAudioPlayer = () => {
  // We are managing promises of audio urls instead of directly storing strings
  // because there is no guarantee when openai tts api finishes processing and resolves a specific url
  // For more info, check this comment:
  // https://github.com/tarasglek/chatcraft.org/pull/357#discussion_r1473470003
  const [queue, setQueue] = useState<Promise<string>[]>([]);
  const [isPlaying, setIsPlaying] = useState<boolean>(false);

  useEffect(() => {
    if (!isPlaying && queue.length > 0) {
      playAudio(queue[0]);
    }
  }, [queue, isPlaying]);

  const playAudio = async (audioClipUri: Promise<string>) => {
    setIsPlaying(true);
    const audioUrl: string = await audioClipUri;
    const audio = new Audio(audioUrl);
    audio.onended = () => {
      URL.revokeObjectURL(audioUrl); // To avoid memory leaks
      setQueue((oldQueue) => oldQueue.slice(1));
      setIsPlaying(false);
    };
    audio.play();
  };

  const addToAudioQueue = (audioClipUri: Promise<string>) => {
    setQueue((oldQueue) => [...oldQueue, audioClipUri]);
  };

  return { addToAudioQueue };
};

export default useAudioPlayer;

Теперь это работает как Charm, когда вы используете этот хук только в одном месте приложения. Но представьте, что произойдет, если я буду использовать этот хук и из какой-то другой части приложения.

const { addToAudioQueue } = useAudioPlayer();

const audioClipUri = textToSpeech(ttsWordsBuffer);
addToAudioQueue(audioClipUri);
  • Будет ли он по-прежнему поддерживать порядок?
  • Оставить порядок в стороне,но есть ли гарантия, что в определенный момент времени воспроизводится только один аудиоклип?

Ответ, как вы уже догадались, НЕТ.

Причина в том, что каждый раз, когда мы вызываем перехватчик useAudioPlayer, он возвращает новый экземпляр аудио очереди. Допустим, вы используете его в трех разных местах своего приложения, теперь у вас одновременно активны 3 аудиоплеера.

И если вы запихнете аудиоклипы на один из них, в то время как другой уже что-то играет, вы попадете в аналогичную ситуацию, как если бы у вас в комнате было 3 динамика, и каждый из них воспроизводил свою песню.

Реализация глобальной аудио очереди

Чтобы исправить эту проблему, мне пришлось убедиться, что во всем приложении всегда существует только один экземпляр аудио очереди, в то время как основная логика и интерфейс остаются прежними.

Это необходимо было сделать в следующих шагах.

Определение Audio Player Context

Первым шагом было определение React Context, который предоставил бы соответствующие функции для работы с аудиоочередью.

type AudioPlayerContextType = {
  addToAudioQueue: (audioClipUri: Promise<string>) => void;
  clearAudioQueue: () => void;
};

const AudioPlayerContext = createContext<AudioPlayerContextType>({
  addToAudioQueue: () => {},
  clearAudioQueue: () => {},
});

Обратите внимание, что я также представляю новый метод, называемый clearAudioQueue. Скоро посмотрим реализацию.

Обертывание очереди аудио в Context Provider

Чтобы любой из компонентов приложения React мог читать из контекста, его сначала необходимо предоставить одному из его предков.

Это делается с помощью React Context Provider. Я обернул существующую логику аудиоплеера в функцию, которая возвращает AudioPlayerContext Provider, предоставляющего функции, которые будут использоваться для работы с одной аудиоочередью в приложении.

import { useState, useEffect, createContext, useContext, ReactNode, FC } from "react";

type AudioClip = {
  audioUrl: string;
  audioElement: HTMLAudioElement;
};

export const AudioPlayerProvider: FC<{ children: ReactNode }> = ({ children }) => {
  // We are managing promises of audio urls instead of directly storing strings
  // because there is no guarantee when openai tts api finishes processing and resolves a specific url
  // For more info, check this comment:
  // https://github.com/tarasglek/chatcraft.org/pull/357#discussion_r1473470003
  const [queue, setQueue] = useState<Promise<string>[]>([]);
  const [isPlaying, setIsPlaying] = useState<boolean>(false);
  const [currentAudioClip, setCurrentAudioClip] = useState<AudioClip | null>();

  useEffect(() => {
    if (!isPlaying && queue.length > 0) {
      playAudio(queue[0]);
    }
  }, [queue, isPlaying]);

  const playAudio = async (audioClipUri: Promise<string>) => {
    setIsPlaying(true);
    const audioUrl: string = await audioClipUri;
    const audio = new Audio(audioUrl);
    audio.preload = "auto";
    audio.onended = () => {
      URL.revokeObjectURL(audioUrl);
      setQueue((oldQueue) => oldQueue.slice(1));
      setIsPlaying(false);

      setCurrentAudioClip(null);
    };
    audio.play();
    setCurrentAudioClip({
      audioElement: audio,
      audioUrl: audioUrl,
    });
  };

  const addToAudioQueue = (audioClipUri: Promise<string>) => {
    setQueue((oldQueue) => [...oldQueue, audioClipUri]);
  };

  const clearAudioQueue = () => {
    if (currentAudioClip) {
      // Stop currently playing audio
      currentAudioClip.audioElement.pause();
      URL.revokeObjectURL(currentAudioClip.audioUrl);

      setCurrentAudioClip(null);
      setIsPlaying(false);
    }

    // Flush all the remaining audio clips
    setQueue([]);
  };

  const value = { addToAudioQueue, clearAudioQueue };

  return <AudioPlayerContext.Provider value={value}>{children}</AudioPlayerContext.Provider>;
};

Вы заметите реализацию функции clearAudioQueue, которая предназначена для вызова из любого компонента приложения для приостановки текущего аудиоклипа и очистки оставшихся клипов в очереди.

Я также сейчас управляю новым состоянием:

type AudioClip = {
  audioUrl: string;
  audioElement: HTMLAudioElement;
};

const [currentAudioClip, setCurrentAudioClip] = useState<AudioClip | null>();

Это позволяет мне pause текущий звук сразу после вызова функции clearAudioQueue.

Предоставление контекста

Следующим шагом будет предоставление файла AudioPlayerContext в корне приложения.

main.tsx
import { AudioPlayerProvider } from "./hooks/use-audio-player";

ReactDOM.createRoot(document.querySelector("main") as HTMLElement).render(
  <React.StrictMode>
    <ChakraProvider theme={theme}>
      <AudioPlayerProvider>
        <SettingsProvider>
          <CostProvider>
            <ModelsProvider>
              <UserProvider>
                <ColorModeScript initialColorMode={theme.config.initialColorMode} />
                <RouterProvider router={router} />
              </UserProvider>
            </ModelsProvider>
          </CostProvider>
        </SettingsProvider>
      </AudioPlayerProvider>
    </ChakraProvider>
  </React.StrictMode>
);

Определение хука useAudioPlayer

После определения контекста и поставщика мы, наконец, можем написать хук, который будет использовать AudioPlayerContext предоставленный ближайшим поставщиком контекста.

const useAudioPlayer = () => useContext(AudioPlayerContext);

export default useAudioPlayer;

Использование хука

И последним шагом было использование глобального контекста аудиоплеера в тех местах, где мне хотелось бы.

В настоящее время я использую его для двух сценариев:

1. Звук предыдущего сообщения мгновенно перестанет воспроизводиться, как только пользователь задаст новый вопрос.

import useAudioPlayer from "../../hooks/use-audio-player";
...
...
const { clearAudioQueue } = useAudioPlayer();
...
...
 // NOTE: we strip out the ChatCraft App messages before sending to OpenAI.
const messages = chat.messages({ includeAppMessages: false });
// Clear any previous audio clips
clearAudioQueue();
const response = await callChatApi(messages, {
  functions,
  functionToCall,
});

2. Любые аудиоклипы, воспроизводимые с помощью useAudioPlayer, мгновенно остановятся, как только пользователь отключит настройку TTS.

import useAudioPlayer from "../../hooks/use-audio-player";
...
...
const { clearAudioQueue } = useAudioPlayer();
...
...
{isTtsSupported && (
  <Tooltip
    label={settings.announceMessages ? "Text-to-Speech Enabled" : "Text-to-Speech Disabled"}
  >
    <IconButton
      type="button"
      size="lg"
      variant="solid"
      aria-label={
        settings.announceMessages ? "Text-to-Speech Enabled" : "Text-to-Speech Disabled"
      }
      icon={
        settings.announceMessages ? <MdVolumeUp size={25} /> : <MdVolumeOff size={25} />
      }
      onClick={() => {
        if (settings.announceMessages) {
          // Flush any remaining audio clips being announced
          clearAudioQueue();
        }
        setSettings({ ...settings, announceMessages: !settings.announceMessages });
      }}
    />
  </Tooltip>
)}

И это все, что нужно для создания и использования пользовательского хука аудиоплеера в приложении React. Я почти уверен, что на npm уже опубликованы хуки для аудиоплееров, но сомневаюсь, что какой-либо из них подойдет моим потребностям, поскольку мне нужно поддерживать очередь.

Дайте знать в комментариях, если вы знаете лучший подход.

Источник:

#React
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу