Как добавить режим Picture-in-Picture в React.JS — обложка

Введение

Picture-in-picture (ПИП) — это отдельное окно браузера с видео, которое можно перемещать за пределы текущей страницы.

Сверните вкладку или даже браузер, где вы смотрите видео, — оно всё равно будет видно в маленьком окне. Удобно, если транслируете экран и хотите видеть собеседников или смотрите очень интересный сериал, но кто-то прислал сообщение.

Как добавить режим Picture-in-Picture в React.JS, image #1

Разберём, как сделать это окно, чтобы оно работало.

На начало 2022 года спецификация Picture-in-Picture находится на стадии черновика. Во всех браузерах эта функция работает по-разному, а поддержка пока оставляет желать лучшего.

На начало 2022 года эту функцию поддерживают всего 48% браузеров.

Как добавить режим Picture-in-Picture в React.JS, image #2

Далее будет технический гайд по реализации со всеми подводными камнями и неожиданными поворотами. Enjoy)

How to PIP

Для начала нужен видеоэлемент

 
 video controls src="video.mp4"> /video
 

В Chrome и Safari должна появиться кнопка для активации PiP.

Как добавить режим Picture-in-Picture в React.JS, image #3

В Хроме нужно нажать на три точки в правом нижнем углу.

Как добавить режим Picture-in-Picture в React.JS, image #4

В Safari нажмите кнопку в левом верхнем углу

5 минут и готово!

Что же насчёт Firefox?

К сожалению, Mozilla пока не поддерживает полноценно режим «картинка в картинке» :(

Чтобы активировать пип в Мозиле, каждому пользователю нужно в перейти в конфигурацию (набрать в поисковой строке about:config). Затем найти media.videocontrols.picture-in-picture.enabled и сделать его true.

Из-за слабой поддержки ПИП в Mozilla, далее мы не будем рассматривать этот браузер.

Теперь вы можете включить ПИП во всех популярных браузерах.

Но что, если этого мало?
Может, можно удобнее?

Может добавить красивую кнопку активации?
Или автоматически переходить в ПИП при уходе со страницы?

Да, это всё можно!

Программная активация ПИП

Для начала реализуем основной функционал открытия и закрытия и подключим кнопку.

Допустим, ваш браузер поддерживает ПИП. Чтобы открыть и закрыть окно ПИП, нам понадобится:

  1. убедиться в поддержке функции
  2. убедиться в отсутствии других ПИПов
  3. реализовать кроссбраузерную функцию включения и выключения режима «картинка в картинке»

Проверка поддержки

Чтобы убедиться, что мы можем программно активировать ПИП-окно, нужно проверить, включён ли он в браузере, и существует ли способ его открытия.

Проверить статус активации можно через свойство document pictureInPictureEnabled:

 
 "pictureInPictureEnabled" in document && document.pictureInPictureEnabled
 

Чтобы убедиться, что мы можем взаимодействовать с окном ПИП, попробуем найти способ его активации.

Для Сафари это webkitSetPresentationMode, для всех остальных браузеров requestPictureInPicture.

 
 export const canPIP = (): boolean => "pictureInPictureEnabled" in document && document.pictureInPictureEnabled;
const supportsOldSafariPIP = () => { const video = document.createElement("video");
 return ( canPIP() && video.webkitSupportsPresentationMode && typeof video.webkitSetPresentationMode === "function" );};
const supportsModernPIP = () => { const video = document.createElement("video");
 return ( canPIP() && video.requestPictureInPicture && typeof video.requestPictureInPicture === "function" )};
const supportsPIP = (): boolean => supportsOldSafariPIP() || supportsModernPIP();
 

Проверка наличия ПИП-окна

Чтобы определить, есть у нас уже picture-in-picture окно или нет, можно посмотреть его в свойствах document

 
 document.pictureInPictureElement
 

Функции открытия и закрытия

Открыть

Стандартный способ вызова метода requestPictureInPicture.

 
 video.requestPictureInPicture();
 

Для большей поддержки среди браузеров реализуем фолбэк. Для входа в picture-in-picture на сафари нужно использовать метод видеоэлемента webkitSetPresentationMode:

 
 video.webkitSetPresentationMode("picture-in-picture")
 

Закрыть

Стандартный метод закрытия:

 
 document.exitPictureInPicture()
 

Фолбэк для Safari:

 
 video.webkitSetPresentationMode("inline")
 

В итоге у нас получается функциональность для открытия и закрытия ПИП.

 
 export const canPIP = () => "pictureInPictureEnabled" in document && document.pictureInPictureEnabled;const isInPIP = () => Boolean(document.pictureInPictureElement);const supportsOldSafariPIP = () => { const video = document.createElement("video"); return ( canPIP() && video.webkitSupportsPresentationMode && typeof video.webkitSetPresentationMode === "function" );};const supportsModernPIP = () => { const video = document.createElement("video"); return ( canPIP() && video.requestPictureInPicture && typeof video.requestPictureInPicture === "function" )};const supportsPIP = () => supportsOldSafariPIP() || supportsModernPIP();export const openPIP = async (video) => { if (isInPIP()) return; if (supportsOldSafariPIP()) await video.webkitSetPresentationMode("picture-in-picture"); if (supportsModernPIP()) await video.requestPictureInPicture();};const closePIP = async (video) => { if (!isInPIP()) return; if (supportsOldSafariPIP()) await video.webkitSetPresentationMode("inline"); if (supportsModernPIP()) await document?.exitPictureInPicture();};
 

Теперь остаётся лишь подключить кнопку.

 
 const disablePIP = async () => { await closePIP(videoElement.current).catch(/*handle error*/)};const enablePIP = async () => { await openPIP(videoElement.current).catch(/*handle error*/)};const handleVisibility = async () => { if (document.visibilityState === "visible") await disablePIP(); else await enablePIP();};const togglePIP = async () => { if (isInPIP()) await disablePIP() else await enablePIP()};
 

Не забываем ловить ошибки в асинхронных функциях и подключим эту функциональность к кнопке.

 
 
 
Как добавить режим Picture-in-Picture в React.JS, image #5

Не так уж много кода — и кнопка для переключения ПИП готова!

Автоматическая активация режима «картинка в картинке»

Зачем нужен picture-in-picture?
Чтобы серфить интернет и смотреть видео с другой страницы!

Общаетесь в видеоконференции в браузере, хотите читать текст из Google Документа, но при этом не терять из виду собеседника — как в Skype. Это возможно с помощью PiP. Или хотите продолжать смотреть фильм, пока отвечаете на срочное сообщение в мессенджере — тоже можно, если сайт, где вы смотрите фильм, поддерживает функцию PiP.

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

В сафари есть свойство autoPictureInPicture, оно переводит в режим Picture-In-Picture только если пользователь смотрит видео фулскрин.

Для его активации нужно сделать свойство видеоэлемента autoPictureInPicture true.

 
 if (video && "autoPictureInPicture" in video) { video.autoPictureInPicture = true;}
 

На этом всё для Safari.

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

Для отслеживания ухода со страницы можно использовать Page Visibility API.

 
 document.addEventListener("visibilitychange", async () => { if (document.visibilityState === "visible") await closePIP(video); else await openPIP(video);});
 

Enjoy, автоактивация режима «картинка в картинке» готова.

Контроль PIP

Пип видео по умолчанию имеет кнопки:

  • паузы (за исключением случаев, когда передаём медиа-стрим в тег видео)
  • перехода обратно на страницу
  • следующего/предыдущего видео

Для настройки переключения видео стоит воспользоваться API Media Session.

 
 navigator.mediaSession.setActionHandler('nexttrack', () => { // set next video src});navigator.mediaSession.setActionHandler('previoustrack', () => { // set prev video src});
 
Как добавить режим Picture-in-Picture в React.JS, image #6

Связка с видеоконференцией

Допустим, вы хотите сделать браузерный Skype с демонстрацией экрана.
Было бы неплохо показать лицо выступающего. И ещё хорошо, чтобы он сам себя видел — вдруг волосы растрепались.

Для этого отлично подойдёт режим «картинка в картинке»!

Чтобы отобразить медиа-стрим в PiP, достаточно применить его к видео — и всё.

 
 video.srcObject = await navigator.mediaDevices.getUserMedia({ video: true, audio: true,})
 
Как добавить режим Picture-in-Picture в React.JS, image #7

Таким простым способом можно показать лицо человека, демонстрирующего экран. А самое удобное — больше не нужно отдельно передавать видео с его лицом, ведь оно уже есть в демонстрации и появляется там, где автор захочет.

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

Такая же логика работает и с собеседником в онлайн-конференции.
Всё, что может отображаться в видеотеге, можно показать и в окне ПИП.

Подводные камни

Ошибка: Failed to execute 'requestPictureInPicture'

DOMException: Failed to execute 'requestPictureInPicture' on 'HTMLVideoElement': Must be handling a user gesture if there isn't already an element in Picture-in-Picture

Значит, браузер понял, что мы злоупотребляем API, или вы забыли проверить, открыто ли окно.
В черновике W3 указаны обязательные условия: userActivationRequired и playingRequired. Это означает, что режим «картинка в картинке» можно активировать только при взаимодействии пользователя и при условии, что видео воспроизводится.

На текущий момент ошибку можно встретить в 2 популярных кейсах:

  1. (Хром) попытка перехода в ПИП, если страница не в фокусе.
  2. (Сафари) попытка перехода в ПИП без взаимодействия пользователя

Видео в ПИП-окне не обновляется

Чтобы справится с этой проблемой в реакт, достаточно поменять свойство key вместе с обновлением медиапотока или src.

 
 video controls key={/* updated key */} src="video.mp4"> /video
 

Видео в ПИП-окне зависает

Периодически видео зависает. Обычно это происходит, когда элемент видео исчезает со страницы. В такой ситуации нужно вызвать метод document.exitPictureInPicture().

При начале трансляции в другой вкладке или приложении окно ПИП не открывается автоматически (Хром)

Эта проблема связана с данной ошибкой. Причина в том, что при клике на системное окно выбора вкладки или страницы для демонстрации наша страница теряет фокус. А без фокуса не выполняется обязательное условие userActivationRequired. Поэтому сразу после начала демонстрации экрана нельзя открыть ПИП.

Однако можно открывать пип-окно заранее, например, при потере фокуса на странице:

 
 document.addEventListener("blur", () => { // open PIP})
 

В таком случае ПИП будет открываться до начала трансляции.

БОНУС. Лайфхак для включения pip в YouTube:

1. включаем видео

2. открываем консоль (нажать Option + ⌘ + J (macOS), или Shift + CTRL + J (Windows/Linux))

3. вводим следующий код

 
 document.onclick = () => { document.querySelector('video')?.requestPictureInPicture(); document.onclick = null;};
 

4. жмем Enter

5. тыкаем в любое пустое место на странице

Enjoy :)

Заключение

Несмотря на довольно слабую поддержку браузерами — всего 48% на начало 2022 года — эта функция легко реализуется и значительно улучшает опыт пользователей веб-приложений с видео или трансляциями.

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

  • Разработка