Аудиоэлемент HTML5: визуальные элементы кнопки для воспроизведения звука не обновляются при нажатии, а ждут, пока звук не начнет воспроизводиться (проблема с мобильным iOS)

Фон

У меня есть кнопка, которая воспроизводит звук, подключенный к ближайшему элементу audio, при обнаружении события click. Элемент button имеет вложенный элемент img, src которого я изменяю, чтобы он указывал на различные файлы .svg в зависимости от того, есть ли аудио.

  1. играю (значок паузы)
  2. приостановлено (значок воспроизведения)
  3. собираюсь играть, но еще не начал играть (вращающийся значок буферизации)

Проблема

Переключение на значок паузы или воспроизведения происходит без проблем, но переключение на значок буферизации происходит так поздно, что это бесполезно (т. е. пользователь нажимает кнопку воспроизведения, пользователь ждет сколько угодно долго, а значок буферизации мигает в течение нескольких мс непосредственно перед началом воспроизведения звука).

На настольном компьютере все происходит очень быстро, и значок буферизации даже не понадобится, но на мобильных устройствах (iOS) может быть задержка от одной до четырех секунд с момента, когда пользователь нажимает кнопку воспроизведения и звук фактически начинает воспроизводиться (что вот почему мне нужен значок буферизации для этого времени ожидания).

Код

HTML-структура

Таких li элементов целая куча. Основное внимание здесь уделяется элементу button.playOrPauseBtn:

<li>
    <audio
        id = "TR-21"
        class = "dialogueAudio"
        src = "/static/audio/en/es/end/tr-21.mp3"
        preload = "auto"
    >
    </audio>
    <div>
        <button type = "button" class = "svgBtn playOrPauseBtn" >
            <img class = "playIcon" src = "/static/svg/play.svg" height = "22px" width = "22px" />
        </button>
    </div>
</li>

javascript

В центре внимания здесь будет прослушиватель событий внизу, который, как я ожидаю, немедленно изменит пользовательский интерфейс кнопки воспроизведения/паузы на значок моего буфера сразу при нажатии кнопки, но на мобильном устройстве (iOS; не тестировал Android) , он на самом деле ждет, а затем только мигает значок буфера прямо перед началом воспроизведения звука (ожидание от 1 до 4 секунд, в зависимости от звука).

Если кажется странным, что все разделено на множество мини-функций, то это потому, что это чрезвычайно упрощенные версии реальных функций (я пытаюсь сосредоточиться только на рассматриваемой проблеме). Они делают с пользовательским интерфейсом гораздо больше, например меняют заголовок и замещающий текст для различных элементов. Дайте мне знать, если вы считаете, что было бы полезно увидеть код JS и разметку HTML в их исходной, неупрощенной форме.

/* ----------------------------------------------
------------------- FUNCTIONS -------------------
---------------------------------------------- */

const changeAudioControlBtn = function(btn, svg, disabled=false, spinny=false) {
  // get other elements
  let btnImg = btn.querySelector("img");
  // change UI of btn
  btn.disabled = disabled;
  // change UI of img
  btnImg.src = svg;
  if (spinny === false) {
    btnImg.classList.remove("spinny");
  } else {
    btnImg.classList.add("spinny");
  }
}

/* Function to pause all audios */
const pauseAll = async function() {
  let allYeAudios = document.querySelectorAll("audio");
  allYeAudios.forEach((item, i) => {
    item.pause();
  });
};

/* Function for playing or pausing audio associated with play/pause button */
const playOrPause = async function(soundByte) {
  if (soundByte.paused == true) {
    // pause other audios before playing
    await pauseAll();
    soundByte.play();
  } else {
    soundByte.pause();
  }
};

/* Function to change play/pause button icon to play or pause symbol */
const changePlayOrPauseGraphics = function() {
  let playPauseBtn = this.parentElement.querySelector(".playOrPauseBtn");
  if (this.paused === true) {
    // set properties for function to change UI of play/pause btn
    let btn = playPauseBtn;
    let svg = "/static/svg/play.svg";
    changeAudioControlBtn(btn, svg);
  } else {
    // set properties for function to change UI of play/pause btn
    let btn = playPauseBtn;
    let svg = "/static/svg/pause.svg";
    changeAudioControlBtn(btn, svg);
  }
};

/* ----------------------------------------------
---------------- EVENT LISTENERS ----------------
---------------------------------------------- */

const allDialogueAudios = document.querySelectorAll(".dialogueAudio");

// Change visuals if playing
allDialogueAudios.forEach((item, i) => {
  item.addEventListener("playing", changePlayOrPauseGraphics);
});

// change visuals when audio pauses too
allDialogueAudios.forEach((item, i) => {
  item.addEventListener("pause", changePlayOrPauseGraphics);
});

const allPlayButtons = document.querySelectorAll(".playOrPauseBtn");

// Play buttons
allPlayButtons.forEach((item, i) => {
  item.addEventListener("click", () => {
    // set properties for UI function (to change btn to spinny buffer icon)
    let btn = item;
    let svg = "/static/svg/loady_spinner.svg";
    let disabled = true;
    let spinny = true;
    changeAudioControlBtn(btn, title, svg, alt, disabled, spinny);
    let soundByte = item.parentElement.parentElement.querySelector(".dialogueAudio");
    playOrPause(soundByte);
  });
});

Что я пробовал

Раньше у меня был код, который вызывает функцию changeAudioControlBtn(), чтобы изменить пользовательский интерфейс playOrPauseBtn на значок буфера внутри функции playOrPause(), а обработчик прослушивателя событий для кнопок воспроизведения/паузы был намного проще (он просто вызывал функцию playOrPause() , но я думал, что перемещение кода изменения значка буфера в обработчик прослушивателя событий будет более быстрым. Но это не дало никакого эффекта.

Другие детали

Похоже, что проблема не связана с Safari, поскольку она не возникает на настольном компьютере Safari, и та же проблема возникает на моем устройстве iOS в браузерах Chrome, Safari и Firefox.

Есть ли конкретная причина, по которой SVG добавляется с использованием элемента <img>, а не в фоне CSS или чего-то подобного? А что делает класс spinny на btnImg?

chrwahl 15.06.2024 22:32

@chrwahl, у меня всегда был элемент <img> внутри кнопок управления звуком. Будет ли большое преимущество в отображении значков кнопок с использованием фона CSS или чего-то в этом роде? Класс spinny просто имеет CSS-анимацию вращения, и все.

DevinG 15.06.2024 23:23
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
0
2
104
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

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

https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/canplaythrough_event

Попробуйте этот подход:

// Play buttons allPlayButtons.forEach((item, i) => {   item.addEventListener("click", () => {
    // set properties for UI function (to change btn to spinny buffer icon)
    let btn = item;
    let svg = "/static/svg/loady_spinner.svg";
    let disabled = true;
    let spinny = true;
    changeAudioControlBtn(btn, title, svg, alt, disabled, spinny);
    let soundByte = item.parentElement.parentElement.querySelector(".dialogueAudio");
    soundByte.addEventListener('canplaythrough', () => {
      playOrPause(soundByte);
    });   }); });

В этом коде функция playOrPause вызывается только при возникновении события canplaythrough, гарантируя, что звук готов к воспроизведению. Это должно помочь отобразить значок буферизации сразу после нажатия кнопки и сохранить его видимым до тех пор, пока не начнет воспроизводиться звук. Кроме того, лучше протестировать свой код на нескольких платформах, чтобы убедиться, что он работает должным образом.

Мне бы очень хотелось использовать событие «canplaythrough», но оно не работает должным образом на устройствах iOS :(

DevinG 17.06.2024 16:38

Попробуйте использовать событие loadedmetadata: если событие canplaythrough не запускается должным образом, вы можете вместо этого прослушать событие loadedmetadata. Это событие генерируется при загрузке метаданных и может быть более надежным на устройствах iOS. video.addEventListener('loadedmetadata', startPlaying);ссылка из этого поста

TitoSenpai 18.06.2024 12:54

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

if (spinny) {
     btnImg.classList.add("spinny");
}

btnImg.classList.remove("spinny");
Ответ принят как подходящий

Суть этой проблемы заключается в том, что звук не загружается в iOS, пока вы не вызовете метод load(), и поэтому canplay, canplaythrough и т. д. не запускаются. Либо вы можете вызывать load() для всех аудиообъектов на странице при загрузке DOM, либо вы можете вызывать load() каждый раз, когда пользователь нажимает кнопку для воспроизведения аудио. В дальнейшем я выбрал последнее, потому что это трудный путь, и если у вас много аудиообъектов, я думаю, это дает меньше сетевого трафика.

Первоначальный вид кнопок — значок воспроизведения. Аудиоэлементы могут иметь атрибут preload со значением none или auto. Насколько я понимаю, это влияет только на браузеры, отличные от iOS.

В playOrPause() я проверяю, имеет ли аудиообъект пользовательское свойство loaded со значением true, если нет, то это должно быть так, потому что это первый раз, когда кнопка нажимается, а canplay не срабатывает (в случае iOS). Свойством manuallyloaded я указываю, что load() вызывался «вручную». Если loaded не установлен, будет выполняться исходное условие для паузы/не паузы.

В changePlayOrPauseGraphics() я сделал оператор переключения для различных событий, изменив имя класса кнопки между play, pause и spinny. Для событий pause, canplay и ended на кнопке должен отображаться значок воспроизведения, и в то же время существует условие для manuallyloaded. Если manuallyloaded равен true, это означает, что load() был вызван и что кнопка была нажата в первый раз, и, следовательно, следует продолжить воспроизведение звука (и удалить свойство manuallyloaded). В противном случае, если событие является canplay, свойство loaded следует просто установить без воспроизведения звука.

Единственная ситуация, в которой вы увидите вращающийся значок, — это когда событие emptied запускается, и это произойдет только в том случае, если load() вызывается. Таким образом, либо в случае iOS-браузера, либо если пользователь в других браузерах нажимает кнопку до начального события canplay (либо потому, что предварительная загрузка атрибута равна none, либо потому, что у пользователя медленное соединение и у него быстрая рука).

Комментарий к слушателям событий

Как для событий щелчка, так и для всех событий, связанных со звуком, я добавляю прослушиватель событий к родительскому элементу (здесь audiolist01). Все события в любом случае перемещаются вверх и вниз по дереву DOM. Причина, по которой я часто использую родительский элемент, заключается в том, что 1) мне не нужно перебирать N элементов, 2) если что-то добавляется в DOM динамически, мне не нужно добавлять прослушиватели событий для этого конкретного элемента. . Единственным недостатком является то, что для общих событий, таких как click, мне приходится проверять, по какому элементу был сделан щелчок, как вы можете видеть в обратном вызове события click. Что касается аудиособытий, я знаю, что они могут исходить только от аудиообъекта. Обратите внимание, кстати, что мне нужно использовать параметр useCapture для addEventListener() для аудио — я не могу объяснить, почему это необходимо, но почему-то события присутствуют только на фазах всплеска (addEventListener: useCapture).

const audiolist01 = document.getElementById('audiolist01');

/* ----------------------------------------------
------------------- FUNCTIONS -------------------
---------------------------------------------- */

/* Function for any click event on the audiolist */
const audiolistClickHandler = function(e) {
  switch (e.target.name) {
    case 'playOrPauseBtn':
      playOrPause(e);
      break;
  }
};

/* Function for playing or pausing audio associated with play/pause button */
const playOrPause = async function(e) {
  let soundByte = e.target.closest('li').querySelector('audio');

  if (!soundByte.loaded) {
    soundByte.load();
    soundByte.manuallyloaded = true;
  } else {
    if (soundByte.paused) {
      // pause other audios before playing
      await pauseAll();
      soundByte.play();
    } else {
      soundByte.pause();
    }
  }
};

/* Function to change play/pause button icon to play or pause symbol */
const changePlayOrPauseGraphics = function(e) {
  let playPauseBtn = e.target.closest('li').querySelector(".playOrPauseBtn");
  playPauseBtn.disabled = false;
  switch (e.type) {
    case 'playing':
      playPauseBtn.classList.replace('play', 'pause');
      playPauseBtn.classList.replace('spinny', 'pause');
      break;
    case 'pause':
    case 'canplay':
    case 'ended':
      if (e.target.manuallyloaded) {
        delete e.target.manuallyloaded;
        e.target.play();
        e.target.loaded = true;
      } else if (e.type == 'canplay'){
        e.target.loaded = true;
      }
      playPauseBtn.classList.replace('pause', 'play');
      playPauseBtn.classList.replace('spinny', 'play');
      break;
    default:
      // effectively only the emptied event
      playPauseBtn.disabled = true;
      playPauseBtn.classList.replace('play', 'spinny');
      playPauseBtn.classList.replace('pause', 'spinny');
      break;
  }
};

/* Function to pause all audios */
const pauseAll = async function() {
  let allYeAudios = document.querySelectorAll("audio");
  allYeAudios.forEach((item, i) => {
    item.pause();
  });
};

/* ----------------------------------------------
---------------- EVENT LISTENERS ----------------
---------------------------------------------- */

audiolist01.addEventListener('click', audiolistClickHandler);

// Change visuals if playing
audiolist01.addEventListener("playing", changePlayOrPauseGraphics, true);

// change visuals when audio pauses too
audiolist01.addEventListener("pause", changePlayOrPauseGraphics, true);

audiolist01.addEventListener("canplay", changePlayOrPauseGraphics, true);
audiolist01.addEventListener("emptied", changePlayOrPauseGraphics, true);
audiolist01.addEventListener("ended", changePlayOrPauseGraphics, true);
ul.audiolist {
  list-style: none;
  margin: 0;
  padding: 0;
}

button.svgBtn {
  padding: 0;
  border: none;
  background-color: transparent;
  width: 22px;
  height: 22px;
}

button.svgBtn.play {
  background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj4KICA8Y2lyY2xlIGZpbGw9Im9yYW5nZSIgY3g9IjUwIiBjeT0iNTAiIHI9IjUwIi8+CiAgPHBhdGggc3Ryb2tlPSJmaWxsIiBkPSJNIDMwIDE1IHYgNzAgbCA1MCAtMzUgeiIvPgo8L3N2Zz4KCg==');
}

button.svgBtn.pause {
  background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj4KICA8Y2lyY2xlIGZpbGw9Im9yYW5nZSIgY3g9IjUwIiBjeT0iNTAiIHI9IjUwIi8+CiAgPHBhdGggc3Ryb2tlPSJibGFjayIgZD0iTSAzMCAyMiB2IDU2IG0gNDAgMCB2IC01NiIgc3Ryb2tlLXdpZHRoPSIxOCIvPgo8L3N2Zz4KCg==');
}

button.svgBtn.spinny {
  background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj4KICA8Y2lyY2xlIGZpbGw9Im9yYW5nZSIgY3g9IjUwIiBjeT0iNTAiIHI9IjUwIi8+CiAgPGNpcmNsZSBmaWxsPSJub25lIiBzdHJva2U9ImJsYWNrIiBzdHJva2Utd2lkdGg9IjE1IiBjeD0iNTAiIGN5PSI1MCIgcj0iMzUiIHBhdGhMZW5ndGg9IjEwMCIgc3Ryb2tlLWRhc2hhcnJheT0iNSA1IDEwIDUgMjAgNSAzMCAxMDAiLz4KPC9zdmc+Cg==');
  animation: 1s infinite rotate linear;
}

@keyframes rotate {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}
<ul id = "audiolist01" class = "audiolist">
  <li>
    <audio id = "TR-21" class = "dialogueAudio" src = "https://cdn.pixabay.com/audio/2023/08/24/audio_8d3bddfb3b.mp3" preload = "auto">
    </audio>
    <div>
      <button type = "button" name = "playOrPauseBtn" class = "svgBtn playOrPauseBtn play"></button>
    </div>
  </li>
  <li>
    <audio id = "TR-22" class = "dialogueAudio" src = "https://cdn.pixabay.com/audio/2023/08/17/audio_64ceae974a.mp3" preload = "none">
    </audio>
    <div>
      <button type = "button" name = "playOrPauseBtn" class = "svgBtn playOrPauseBtn play"></button>
    </div>
  </li>
</ul>

Пара дополнительных вопросов: 1) почему бы вам не беспокоиться об отключении кнопки, пока значок буфера активен (т. е. звук не готов)? 2) есть ли причина, помимо простоты реализации, по которой вы поместили прослушиватель событий «щелчок» на весь раздел, содержащий аудиоэлементы? Другими словами, более эффективно это для машины или просто для вас как разработчика?

DevinG 19.06.2024 02:56

Объявление. 1: Я добавил код для отключения кнопки. Ну, это не было целью задачи... Объявление. 2: Я добавил объяснение того, как я использую прослушиватели событий.

chrwahl 19.06.2024 16:19

Другие вопросы по теме