Шрирам Рао
Приложение Netflix TV воспроизводит видео не только тогда, когда пользователи решают посмотреть заголовок, но и во время просмотра в поисках чего-то интересного. Когда мы переписали пользовательский интерфейс телевизора в React, мы решили улучшить опыт разработчиков, интегрировав воспроизведение видео в пользовательский интерфейс, чтобы мы могли быстрее экспериментировать с различными видео-ориентированными пользовательскими интерфейсами.

Ключевым моментом было предоставить декларативный интерфейс для базового API императивного воспроизведения видео, который более выразителен, легче расширяется и лучше скрывает сложность взаимодействия с системой с отслеживанием состояния, что более элегантно вписывается в наше приложение React.
Распространенный метод воспроизведения видео в React включает рендеринг элемента <video> и последующее использование ссылки для вызова императивных действий. Хорошим примером этого является переход к другому месту в текущем видео. На самом деле у нас нет <video> элемента, доступного для нашего пользовательского интерфейса, только обязательный API видеопроигрывателя на платформе устройств Netflix. Мы решили отобразить этот императивный API / API с отслеживанием состояния на полностью декларативный компонент React без сохранения состояния, потому что он лучше подходит для React, поскольку JSX является декларативным, позволяет сохранять состояние видео в состоянии приложения, помогает избежать использования ссылок и позволяет более легкое наслоение функциональности с помощью компонентов более высокого порядка (например, для добавления текста в речь).
Пункт назначения против маршрута
Так как же нам избавиться от императивных действий? Вместо того, чтобы организовывать вызовы императивных функций, мы отправляем реквизиты нашему видеокомпоненту, которые описывают желаемое целевое состояние воспроизведения видео. Наш видеокомпонент обнаруживает изменения в реквизитах и решает, какие функции API видеопроигрывателя вызывать.
Наш видеокомпонент сигнализирует об изменениях состояния видео (например, при переходе от загрузки к воспроизведению) и изменении положения воспроизведения. Обработчики этих изменений передаются в props.
Привет, видео!
Компонент, который объединяет императивный API с декларативным фасадом, должен быть способен преобразовывать изменения входных данных в правильную последовательность вызовов императивного API.
Вот как вы можете указать нашему видео компоненту начать воспроизведение в JSX:
<Video
playbackState="playing"
src={videoUrl}
onVideoStateChanged={this.handleVideoStateChanged}
onReportedTimeChanged={this.handleTimeChanged}
/>
playbackState prop - это желаемое конечное состояние. Свойства onVideoStateChanged и onReportedTimeChanged являются обработчиками для нашего видеокомпонента, чтобы сигнализировать об изменениях состояния видео и места воспроизведения соответственно.
Запрошенное видео воспроизводится независимо от текущего состояния воспроизведения. Если видео еще не воспроизводится, оно запускается. Если видео приостановлено, оно возобновляется. Наш видеокомпонент вызывает правильный набор императивных вызовов API для достижения этой цели.
Чтобы приостановить или остановить видео, вызывающая сторона устанавливает для свойства воспроизведенияState значение "paused" или "stopped".
Изменения состояния видео
Наш видеокомпонент передает состояние воспроизведения видео (например, loading, playing, paused и т. Д.), Чтобы окружающие элементы пользовательского интерфейса могли адаптироваться или отражать это состояние.
Мы обнаружили, что компонент хорошо вписывается в декларативный пользовательский интерфейс, если он:
- скрывает сложность и вариативность базовой императивной подсистемы, обеспечивая согласованные состояния и переходы
- преобразует изменения в базовой системе в состояния, которые имеют смысл для пользовательского интерфейса
Примером последнего является то, что переключение с одного воспроизводимого видео на другое вызовет следующие изменения состояния в видеопроигрывателе:

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

Расширение декларативного API
В большинстве случаев расширение API связано с введением дополнительных свойств. Вот как это выглядит при расширении на звуковую дорожку, дорожку субтитров и громкость.
<Video
playbackState="playing"
src={videoUrl}
audioTrack={audioTrackId}
subtitleTrack={subtitleTrackId}
volume={volumeLevel}
onVideoStateChanged={this.handleVideoStateChanged}
onReportedTimeChanged={this.handleTimeChanged}
/>
Моделирование динамических атрибутов
Расширить API для описания целевого значения постоянно меняющегося атрибута системы непросто. Такие атрибуты требуют двух свойств для его описания.
Например, одного свойства time недостаточно, потому что оно может очень быстро устареть. После того, как видео начинается в позиции, указанной свойством time, фактическое место воспроизведения и свойство time расходятся. Также невозможно заставить видео искать обратно в позицию, указанную свойством time, потому что значение этого свойства не изменится.
Наше решение состоит в том, чтобы использовать два свойства времени - time и reportedTime, где reportedTime служит прокси для фактического места воспроизведения и помогает справиться с дрейфом между ними. Вы можете думать о time как о сеттере и о reportedTime как о получателе (но тот, который периодически доставляется через событие). Во время воспроизведения пользовательский интерфейс устанавливает time на reportedTime, поэтому оба значения остаются неизменными. Когда пользовательский интерфейс хочет перейти в другое место, он устанавливает time на целевое время. Изменения в желаемой позиции могут быть обнаружены, потому что time не будет соответствовать reportedTime.
Вот иллюстрация того, как это работает. Скажем, наш видеокомпонент отображается при каждом обновлении до reportedTime (что, кстати, не является обязательным). Поскольку time и reportedTime продвигаются вместе, это фактически не работает. Когда time отличается от reportedTime, инициируется пропуск, и наш видеокомпонент больше не отправляет обновления времени. Любые рендеры во время пропуска также фактически не выполняются, если эти свойства не меняются. После завершения пропуска обновления времени возобновляются, в результате чего time и reportedTime снова синхронизируются.

Более простая ментальная модель
Пользовательский интерфейс может перестать отслеживать текущее состояние и вместо этого сосредоточиться на описании целевого состояния. Наш видеокомпонент всегда получает полное и последнее целевое состояние в props каждый раз, что означает:
- пользовательский интерфейс не должен дросселировать, то есть ему не нужно ждать, пока будет применено одно изменение, прежде чем указывать дополнительные изменения в целевом состоянии.
- наш видеокомпонент должен ставить в очередь только последнее целевое состояние, пока он применяет предыдущее изменение целевого состояния.
Пользовательскому интерфейсу также не нужно заботиться о порядке операций. Он может изменять несколько свойств и ожидать, что они будут применены в правильном порядке, например, когда звуковая дорожка изменяется и состояние воспроизведения изменяется с «приостановлено» на «играет».
Полностью декларативный API не лишен недостатков. Раздутие собственности - это то, чего нужно остерегаться. Моделирование динамических атрибутов может быть не интуитивно понятным и может привести к дополнительным циклам рендеринга, вызванным увеличением частоты обновления состояния (как событие изменения reportTime). Наш опыт показывает, что это не препятствие и не мешает нам использовать наш видеокомпонент даже на недорогих устройствах.
Декларативно за пределами React
Создание декларативного фасада над императивным API в форме компонента React упрощает интеграцию в пользовательский интерфейс. Но это также может означать, что декларативный фасад, возможно, должен учитывать различия в поддержке функций на уровне платформы / устройства и планировать откат. Если у вас есть императивный API, подумайте о том, чтобы превратить этот императивный API в декларативный.
Мы сделали именно это, заменив императивный API нашего видеопроигрывателя одним setTargetState методом, который принимает те же свойства, что и наш видеокомпонент. Это позволило нашему видеокомпоненту рассматривать наш видеопроигрыватель как компонент реакции, передавая ему реквизиты и позволяя ему обнаруживать и реагировать на изменения в реквизитах. Это способствовало лучшему разделению задач как между частями программного обеспечения, так и между командами. Видеопроигрыватель находится в гораздо лучшем положении для принятия решения о действиях, необходимых для перехода из текущего состояния в желаемое целевое состояние, поскольку он имеет доступ к внутреннему состоянию проигрывателя.
Есть альтернативы, даже если у вас нет императивного API. Например, вы можете инкапсулировать эту дисперсию за пределами границ React, на уровне между декларативным компонентом и фактическим императивным API.
Лучший опыт пользователей и разработчиков
По мере того, как мы создаем новые концепции пользовательского интерфейса для ТВ, нам необходимо решать сложные задачи пользовательского интерфейса. Наш компонент декларативного видео сделал более обширную интеграцию видео в пользовательский интерфейс ТВ проще и быстрее.
Вы хотите помочь нам изобрести будущий пользовательский интерфейс для гостиной? Присоединяйтесь к нам, если это пробудит ваше любопытство.