Медиа-контроллер
Разработка для широкого спектра браузеров и устройств часто сопряжена с уникальными проблемами. Одной из наиболее заметных проблем является обеспечение стабильного воспроизведения мультимедиа, особенно на устройствах iOS от Apple.
Объяснение iOS-ограничений воспроизведения мультимедиа
iOS от Apple установила особые правила воспроизведения аудио и видео, чтобы улучшить взаимодействие с пользователем и управлять потреблением данных:
- Удобство пользователя: iOS запрещает автоматическое воспроизведение мультимедиа во избежание непредвиденных сбоев. Такой выбор конструкции гарантирует, что пользователи не будут неожиданно отвлечены внезапным звуком, особенно в тихих условиях.
- Эффективность данных. Учитывая, что потоковое мультимедиа может требовать большого объема данных, iOS требует от пользователей вручную инициировать воспроизведение мультимедиа. Этот дизайн особенно выгоден для пользователей с ограниченными или лимитированными тарифными планами.
Расшифровка проблемы iOS-воспроизведения
В основе проблемы iOS лежит настойчивое требование системы к воспроизведению мультимедиа по инициативе пользователя. Например, запуск видео должен быть результатом непосредственного действия пользователя, например касания. Однако асинхронные обработчики событий, такие как onClick$
(QRL), могут создать разрыв между действием пользователя и командой воспроизведения. Это часто приводит к тому, что iOS блокирует воспроизведение при первом касании, и требуется второе, неинтуитивное касание.
playsinline
Атрибут На устройствах iOS, особенно на iPhone, видео автоматически воспроизводится в полноэкранном режиме, если в теге <video>
не присутствует атрибут playsinline
. Этот атрибут гарантирует, что видео воспроизводится в назначенном контейнере, обеспечивая единообразную работу на разных устройствах и платформах. Хотя в первую очередь playsinline
предназначен для iOS, он безвреден для других платформ и обеспечивает более широкую совместимость.
Создание универсального решения для воспроизведения
Чтобы добиться согласованного воспроизведения мультимедиа в различных браузерах и устройствах, необходим целостный подход. В этом руководстве представлено решение, учитывающее нюансы различных платформ. Чтобы обеспечить немедленное воспроизведение при взаимодействии с пользователем, метод .play()
должен вызываться синхронно. Этого можно добиться, напрямую подключив обработчик click
к функции useVisibleTask$
.
Ниже вы найдёте прототип аудио- и видеоконтроллера, предназначенный для обеспечения единообразного взаимодействия с пользователем на нескольких платформах.
Код универсального медиа-контроллера, совместимого с устройствами iOS, выглядит следующим образом:
import {
component$,
useSignal,
useStylesScoped$,
useVisibleTask$,
} from '@builder.io/qwik';
import { useLocation } from '@builder.io/qwik-city';
const AUDIO_SRC =
'https://cdn.builder.io/o/assets%2F5b8073f890b043be81574f96cfd1250b%2Fafe011812da146a5b2263196cb25f263?alt=media&token=c017cd87-0598-4af2-8afd-e9b5a3fba078&apiKey=5b8073f890b043be81574f96cfd1250b';
const VIDEO_SRC =
'https://cdn.builder.io/o/assets%2F5b8073f890b043be81574f96cfd1250b%2F8b210c56974440649a0a78d4a3a0ddc5%2Fcompressed?apiKey=5b8073f890b043be81574f96cfd1250b&token=8b210c56974440649a0a78d4a3a0ddc5&alt=media&optimized=true';
export default component$(() => {
const audioElementSignal = useSignal<HTMLAudioElement | undefined>();
const audioPlayButtonSignal = useSignal<HTMLButtonElement | undefined>();
const audioIsPlayingSignal = useSignal(false);
const videoElementSignal = useSignal<HTMLAudioElement | undefined>();
const videoPlayButtonSignal = useSignal<HTMLButtonElement | undefined>();
const videoIsPlayingSignal = useSignal(false);
const playsInlineSignal = useSignal(true);
const location = useLocation();
const videoPoster =
location.url.origin + '/sample-media/qwik-koi-poster.jpg';
useStylesScoped$(`
segment {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
width: 100%;
padding: 20px;
color: #1dacf2
}
.content {
width: 60%;
min-width: 250px;
}
button {
padding: 20px;
font-weight: bold;
font-size: 1.2em;
width: 100%;
background: #1dacf2;
color: white;
}
.checkbox-container {
display: flex;
align-items: center;
justify-content: center;
}
.checkbox {
width: 20px;
height: 20px;
margin-right: 8px;
}
.video-container {
position: relative;
width: 100%;
height: 0;
padding-bottom: calc(56.25% + 1px);
}
video {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
box-sizing: border-box;
border: 1px solid gray;
}
`);
useVisibleTask$(({ track }) => {
track(() => audioPlayButtonSignal.value);
track(() => audioElementSignal.value);
const play = () =>
audioIsPlayingSignal.value
? audioElementSignal.value?.pause()
: audioElementSignal.value?.play();
audioPlayButtonSignal.value?.removeEventListener('click', play);
audioPlayButtonSignal.value?.addEventListener('click', play);
return () =>
audioPlayButtonSignal.value?.removeEventListener('click', play);
});
useVisibleTask$(({ track }) => {
track(() => videoPlayButtonSignal.value);
track(() => videoElementSignal.value);
const play = () =>
videoIsPlayingSignal.value
? videoElementSignal.value?.pause()
: videoElementSignal.value?.play();
videoPlayButtonSignal.value?.addEventListener('click', play);
return () =>
videoPlayButtonSignal.value?.removeEventListener('click', play);
});
return (
<segment>
<div class="content">
<h1>Медиа-контроллер</h1>
<h3>
<i>с поддержкой iOS</i>
</h3>
<br />
<div class="video-container">
<video
ref={videoElementSignal}
src={VIDEO_SRC}
poster={videoPoster}
playsInline={playsInlineSignal.value}
onPlay$={() => (videoIsPlayingSignal.value = true)}
onPause$={() => (videoIsPlayingSignal.value = false)}
onEnded$={() => (videoIsPlayingSignal.value = false)}
/>
</div>
<audio
ref={audioElementSignal}
src={AUDIO_SRC}
onPlay$={() => (audioIsPlayingSignal.value = true)}
onPause$={() => (audioIsPlayingSignal.value = false)}
onEnded$={() => (audioIsPlayingSignal.value = false)}
/>
<br />
<div class="checkbox-container">
<input
type="checkbox"
id="playsInlineCheckbox"
class="checkbox"
checked={playsInlineSignal.value}
onchange$={() => {
videoElementSignal.value?.pause();
playsInlineSignal.value = !playsInlineSignal.value;
}}
/>
<label for="playsInlineCheckbox">playsInline (iOS)</label>
</div>
<br />
<button ref={videoPlayButtonSignal}>
{videoIsPlayingSignal.value ? 'Пауза' : 'Воспроизведение'} Видео
</button>
<br />
<br />
<button ref={audioPlayButtonSignal}>
{audioIsPlayingSignal.value ? 'Пауза' : 'Воспроизведение'} Аудио
</button>
</div>
</segment>
);
});