Лента роликов на iOS: опыт работы с AVPlayer

от автора

Привет, Хабр! На связи Дмитрий Андриянов, iOS-разработчик команды Медиа Платформы в Т-Банке. Одна из наших ключевых задач — разработка видеоплеера для стриминговых видео, в том числе для приложения T-Инвестиций, где есть лента тематических роликов и обучающие видео в Академии.

В T-Инвестициях на iOS лента коротких роликов — это 90% трафика, именно ее оптимизации мы уделяем больше всего внимания. В первой версии пользователи могли ждать старта видео больше 15 секунд. Мы разобрались в ограничениях AVPlayer, оптимизировали буферизацию и снизили задержки на 12% в хвосте распределения. Рассказываю, как мы улучшали пользовательский опыт и почему AVPlayer не лучшее решение для ленты роликов.

Особенности UX в ленте коротких роликов

Лента коротких роликов не просто «видео в списке». У сценария просмотра пользователем видео есть особенности, которые влияют на то, как должен работать плеер. 

Посмотрим на особенности UX в ленте коротких роликов: 

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

  • Пользователи часто не досматривают видео до конца, около 40% просмотров завершаются в первые три секунды.

  • По умолчанию звук в ленте выключен и включается по тапу пользователя. 

  • Нет настройки для смены качества — пользователь не может выбрать его вручную.

Слева — лента роликов, справа — полноэкранный плеер в Академии

Слева — лента роликов, справа — полноэкранный плеер в Академии

Проблема долгого старта

При запуске первой версии ленты роликов мы использовали AVPlayer, воспроизводящий видео в формате HLS (HTTP Live Streaming). Чтобы ускорить старт воспроизведения после перехода к следующему видео, мы использовали пул из трех плееров: один для воспроизведения текущего видео, второй для предзагрузки следующего и третий для возвращения к предыдущему ролику. 

Пользователи быстро начали жаловаться на большую задержку перед стартом воспроизведения на iOS: иногда приходилось ждать более 15 секунд. Мы посмотрели аналитику и увидели, что медианное время готовности плеера составляло 750 миллисекунд, но в 10% случаев превышало 2,5 секунды, а в 1% — более 9 секунд.

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

При низкой пропускной способности сети плеер может чрезмерно долго накапливать буфер перед стартом. На коротких роликах это иногда приводит к тому, что загружается практически всё видео, — именно это и было одной из причин долгого ожидания на слабых сетях. Если отключить настройку, плеер начнет воспроизведение сразу, как только в буфере появятся первые данные. Но при исчерпании буфера воспроизведение будет автоматически приостанавливаться — из-за чего пользователь будет часто видеть индикатор загрузки. Отключать настройку мы не стали: частые ребуферинги ухудшили бы пользовательский опыт еще сильнее. Вместо этого мы сосредоточились на доработке логики предзагрузки.

Ограничение буферизации при предзагрузке

Для быстрого старта воспроизведения при предзагрузке достаточно загрузить плейлисты и первый сегмент следующего ролика. Но плеер продолжал буферизовать данные сверх необходимого. К этому добавлялось то, что предыдущий ролик после перехода тоже мог продолжать загрузку. В итоге два неактивных плеера конкурировали с активным за сеть — мы хотели это исправить.

Для регулирования предпочтительного уровня буферизации AVPlayerItem предоставляет настройку preferredForwardBufferDuration. По умолчанию его значение равно 0 — плеер загружает столько данных, сколько считает необходимым для плавного воспроизведения. 

По нашей аналитике, при настройках по умолчанию в 10% случаев размер буфера превышал 55 секунд, а в 1% — даже 97 секунд. Учитывая, что короткие ролики пользователи часто не досматривают до конца, большая часть этих данных не использовалась. Важно понимать, что это лишь рекомендация для плеера, а не жесткое ограничение.

Мы выставили preferredForwardBufferDuration в 1 секунду для неактивных плееров. AVPlayer действительно стал менее агрессивно заполнять буфер, но все равно загружал данные на 12—18 секунд вперед — уменьшить буфер сильнее не удалось.

Ускорение старта видео на слабой сети

Предзагрузка в списке стартовала сразу после перехода к новому ролику. При хорошем интернете это не создавало проблем, но на слабой сети приводило к тому, что активный ролик стартовал дольше, а риск ребуферинга возрастал. Чтобы устранить эту проблему, мы решили начинать предзагрузку только после того, как статус AVPlayerItem для активного видеоплеера переходил в состояние readyToPlay.

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

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

На сколько снизились задержки и ребуферинги: метрики до и после

Благодаря доработкам в логике предзагрузки и отказу от использования третьего плеера улучшился ряд показателей.

Задержка готовности плеера к воспроизведению. Метрика показывает время от создания AVPlayerItem до перехода его статуса в readyToPlay, то есть сколько времени требуется плееру для загрузки плейлистов и накопления достаточного объема данных в буфере для старта воспроизведения.

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

Перцентиль

До

После

Изменение

p50

0,75 с

0,69 с

−9%

p90

2,5 с

2,13 с

−12%

p99

8,93 с

7,78 с

−12%

Наибольший эффект наблюдается на p90 и p99: время подготовки AVPlayerItem сократилось более чем на 12%. При этом медианное значение изменилось менее заметно. Основные оптимизации сказались именно на хвосте распределения, то есть на пользователях с медленным интернетом, для которых проблема конкуренции за сетевой канал была наиболее острой.

Для сравнения: у Android-клиентов, где используется ExoPlayer, медианное время готовности плеера составляло 770 мс, p90 — 1,64 секунды, p99 — 4,53 секунды. ExoPlayer предоставляет более гибкое API для управления буфером и позволяет точно указать, сколько данных нужно накопить перед стартом воспроизведения, — именно это и обеспечивало такие показатели. Наши результаты после оптимизаций были значительно хуже, и разрыв с Android оставался существенным.

Количество ребуферингов. Ребуферинг — прерывание начавшегося воспроизведения из-за нехватки данных в буфере. По результатам исследования Mux, даже одно такое прерывание снижает количество просмотренного видео на 39%. 

Мы отслеживали долю сессий, в которых пользователи столкнулись с ребуферингом хотя бы раз. После оптимизаций метрика снизилась с 1,6 до 0,25% — улучшение более чем в 6 раз. 

Для сравнения: у Android-клиентов в тот же период эта метрика была на уровне 9%. Закономерный компромисс: ExoPlayer начинал воспроизведение при минимальном буфере, что давало быстрый старт, но на слабых сетях буфер быстро исчерпывается — отсюда и высокий процент ребуферингов.

Доля сессий с ребуферингом до и после оптимизаций

Доля сессий с ребуферингом до и после оптимизаций

Качество видео. Для оценки качества воспроизведения мы отслеживали ширину видео в пикселях. Поскольку все ролики вертикальные, именно ширина определяет качество. Нас интересовало, как часто пользователи видят качество ниже 720p в ленте роликов. На 4G и Wi-Fi в 85% времени воспроизведения качество составляло 720p и выше. После оптимизаций значимых изменений в качестве воспроизведения мы не наблюдали.

Распределение качества видео по типам сети

Распределение качества видео по типам сети

Чего нельзя сделать с AVPlayer: кэш, выбор качества и экономия трафика

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

Мы выяснили, что AVPlayer не предоставляет API для гибкой настройки логики буферизации, а его стандартная логика плохо подходит для сценария с лентой роликов. 

Мы столкнулись и с другими ограничениями:

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

    HLS поддерживает разделение аудио- и видеодорожек. Но наши видео хранятся в формате muxed HLS, где аудио- и видеодорожки объединены в один транспортный поток внутри каждого сегмента, из-за чего загрузить только видеодорожку невозможно. В MPEG-DASH у нас дорожки разделены, если бы мы использовали этот протокол, оптимизация была бы возможна. 

  • Обучающие видео в Академии воспроизводятся в полноэкранном плеере, где есть настройка для выбора качества видео. AVPlayer не позволяет явно выбрать конкретный вариант качества из плейлиста. Можно указать предпочтительный верхний порог разрешения и битрейта с помощью preferredMaximumResolution и preferredPeakBitRate соответственно. Ограничить разрешение или битрейт снизу нельзя. Поэтому для реализации выбора качества пришлось использовать костыль в виде AVAssetResourceLoaderDelegate, загружать и парсить плейлист самостоятельно, а затем удалять из него ненужные варианты.

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

    AVFoundation предоставляет класс AVAssetDownloadURLSession для загрузки HLS-видео, но он ориентирован на другой сценарий — офлайн-воспроизведение. Он сохраняет HLS-ассет как офлайн-ресурс, которым затем управляет сама система. При этом разработчик практически не контролирует, какие сегменты и в каком объеме хранятся на устройстве.
    В ленте коротких роликов такой подход нас не устраивал. Мы хотели реализовать собственный кэш с предсказуемым поведением: ограничивать его размер, самостоятельно определять политику вытеснения данных и использовать уже загруженные медиасегменты во время обычного потокового воспроизведения.

  • При ошибке воспроизведения AVPlayer автоматически очищает AVPlayerLayer, из-за чего вместо последнего кадра отображался черный экран. Настроить это поведение нельзя, хотя по требованию дизайна нам было важно сохранять последний кадр. Вдобавок при повторной попытке воспроизведения приходилось пересоздавать AVPlayerItem — все уже загруженные данные при этом терялись и загружались заново.

Выводы

AVPlayer — отличный инструмент для стандартных сценариев воспроизведения. Но лента коротких роликов предъявляет нестандартные требования: быстрый старт воспроизведения, умная предзагрузка следующего видео без влияния на текущее, кэширование сегментов и экономия трафика. 

В совокупности с описанными ограничениями это и привело нас к разработке собственного видеоплеера для воспроизведения MPEG-DASH-видео, что дало нам полный контроль над поведением плеера и позволило закрыть все описанные потребности. О том, как он работает и как его внедрение отразилось на пользовательских метриках, расскажу в следующей статье. Если у вас есть похожий опыт или остались вопросы — жду в комментариях.

ссылка на оригинал статьи https://habr.com/ru/articles/1053402/