Введение

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 должна появиться кнопка активации пип.

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

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

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

В сафари на кнопку в левом верхнем углу

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

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

К сожалению, Мозила еще не имеет полную поддержку picture-in-picture :(

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

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

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

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

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

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

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

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

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

  1. убедиться в поддержке фичи
  2. убедиться в отсутствии другого ПИПа
  3. реализовать кроссбраузерную функцию активации/деактивации picture-in-picture

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

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

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

 
 "pictureInPictureEnabled" in document && document.pictureInPictureEnabled
 

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

Для Сафари это 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()
 

Фолбэк для сафари:

 
 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

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

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

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

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

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

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

На этом все для Сафари.

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

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

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

Enjoy, автоактивация picture-in-picture готова.

Контроль PIP

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

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

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

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

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

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

Для этого идеально подойдет Picture-in-picture!

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

 
 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 and playingRequired. Это значит, что picture-in-picture может активироваться только при взаимодействии с пользователем и если видео играет.

На текущий момент ошибку можно встретить в 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), or Shift + CTRL + J (Windows/Linux)

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

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

4. жмем Enter

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

Enjoy :)

Заключение

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

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

  • Разработка