Важнейшее из искусств: как мы реализовали проигрывание видео в Облаке Mail.Ru
Некоторое время назад в Облаке Mail.Ru появилась возможность проигрывания видеофайлов. Уже в самом начале работы над этим функционалом мы решили, что будем разрабатывать этакий швейцарский нож: требовалась возможность проигрывать любые видеоформаты и функционирование на всех устройствах, где доступно Облако. Загруженные в Облако видеофайлы можно условно разделить на две категории: «фильмы/сериалы» и «видеоролики пользователей», которые люди снимают на телефоны и видеокамеры — для этого случая особенно характерно разнообразие форматов и кодеков. Без предварительной обработки просмотреть все это на любом устройстве невозможно, например, из-за отсутствия нужного кодека или же размер файла окажется слишком большим.
В этой статье я расскажу о том, как устроено проигрывание видеофайлов в Облаке Mail.Ru и каким путем мы шли, чтобы сделать воспроизведение в Облаке «всеядным» на вход и поддержать максимальное число устройств на выходе.
Ряд сервисов (например, YouTube, социальные сети и прочие) конвертирует пользовательское видео в пригодные для воспроизведения форматы сразу после загрузки. Только после окончания конвертации ролик становится доступным для просмотра, но оригинал при этом удаляется.
В Облаке Mail.Ru используется другой подход: оригинальный файл не удаляется, а конвертируется прямо во время проигрывания. В отличие от специализированных видеохостингов мы не можем удалять оригинал. Почему мы остановились на этом варианте? Облако Mail.Ru — это, в первую очередь, облачное хранилище, и пользователь неприятно удивится, если при скачивании своего видеофайла обнаружит, что его качество ухудшилось или размер изменился хотя бы на один байт. С другой стороны, мы не можем себе позволить хранить предварительно сконвертированные копии всех видеофайлов — это существенно увеличит объем занимаемого пространства. Также нам пришлось бы делать много лишней работы, ведь далеко не все из хранящихся видеофайлов посмотрят хотя бы один раз.
Еще один плюс конвертации «на лету» заключается в том, что если мы захотим изменить настройки конвертации или, например, добавить еще одно возможное качество, то нам не придется переконвертировать старые ролики (что не всегда было бы возможно, ведь оригинала в этом случае уже нет) — все заработает автоматически.
Мы используем формат HLS, разработанный Apple специально для потоковой передачи видео по сети. Идея состоит в том, что каждый видеофайл разделяется на фрагменты произвольной длины, из которых формируется плейлист, где для каждого фрагмента указывается имя и его длительность в секундах. Например, двухчасовой фильм делим на десятисекундные фрагменты — получается 720 небольших отдельных файлов. В соответствии с тем, с какого момента пользователь желает смотреть видео, проигрыватель запрашивает из переданного ему плейлиста нужный файл. Одним из преимуществ формата HLS является то, что пользователю не приходится ожидать начала воспроизведения, пока плеер считывает заголовок цельного видеофайла (в случае полнометражного фильма и мобильного интернета время ожидания могло бы оказаться значительным).
Другая не менее важная возможность, которую предоставляет данный формат, — адаптивный стриминг, позволяющий изменять качество воспроизведения «на лету» в зависимости от скорости интернет-канала пользователя. Например, просмотр начинается в 360p на 3G, а после попадания в зону действия LTE продолжается уже в 720p или 1080p. В HLS это реализовано весьма просто — плееру отдается «главный плейлист», состоящий из плейлистов-фрагментов, где указана минимальная необходимая пропускная способность канала. После скачивания фрагмента видеоплеер вычисляет текущую скорость, и в зависимости от нее принимает решение, в каком качестве загружать следующий фрагмент — в таком же, более низком или же более высоком. На текущий момент мы поддерживаем отдачу в 240p, 360p, 480p, 720p и 1080p.
Бэкенд состоит из серверов трех типов. Первая группа принимает запросы на просмотр: происходит формирование/отдача HLS-плейлистов, раздача готовых сконвертированных фрагментов и постановка задач на конвертацию. Вторая группа — база данных со встроенной логикой (Tarantool). Третья группа серверов — конвертеры, которые получают задачи от базы данных и в ней же отмечаются после выполнения. При поступлении запроса на какой-либо фрагмент видеофайла, первое, что мы делаем — проверяем в базе, нет ли уже готового сконвертированного фрагмента с запрошенным качеством на каком-либо из наших серверов. Тут возможно два варианта.
Первый: фрагмент есть. В этом случае мы сразу его отдаем. Оказаться уже сконвертированным он может при условии, что вы или кто-то другой запрашивал его в течение последних N минут. Это первый уровень кэширования, который работает для всех конвертируемых файлов. Стоит упомянуть, что помимо этого мы используем еще один вид кэширования: файлы, часто запрашиваемые в последнее время, распространяются и отдаются с нескольких серверов, чтобы исключить возможность перегрузок сетевого интерфейса.
Второй вариант: готового сконвертированного фрагмента у нас не оказалось. В этом случае в БД ставится задача на конвертацию, а мы ожидаем, когда она выполнится. Хранением информации о видео и управлением очередью конвертации занимается база данных Tarantool — очень быстрая opensource NoSQL база данных, для которой можно писать хранимые процедуры на Lua. Общение описанного выше сервера с базой происходит следующим образом. Сервер делает в базу запрос «Хочу N-й фрагмент файла M с качеством K, готов ждать не более T секунд», и в течение T секунд он получает информацию о том, откуда можно забирать готовый файл, или же о произошедшей ошибке. Таким образом, клиента базы данных не интересует, как будет выполнена его задача — сразу или через цепочку сложных действий: ему предоставлен максимально простой интерфейс, позволяющий отправить запрос и получить запрошенное.
Fault tolerance базы данных обеспечивается следующим образом: клиент обращается только к мастер-серверу. При возникновении проблем реплика маркируется мастером, и клиент обращается уже к ней. При этом с точки зрения клиента никаких изменений не происходит — он по-прежнему взаимодействует с мастером.
Другим типом клиентов базы являются конвертеры, готовые получить на вход HTTP-ссылку на файл с некоторыми параметрами и сделать из него сконвертированный фрагмент. Общение этих конвертеров с базой происходит схожим образом: отправляется запрос «Дай мне задачу, я готов ждать N секунд», и если за эти N секунд задача появится, то она мгновенно будет отдана одному из ожидающих конвертеров. Механизм передачи задач от клиента к конвертеру удалось весьма просто реализовать с помощью IPC Channel’ов в Lua внутри Tarantool’a, позволяющих осуществлять взаимодействие между разными запросами. Вот упрощенный код получения сконвертированного фрагмента:
function get_part(file_hash, part_number, quality, timeout)
-- Пытаемся заселектить запрошенный кусочек
local t = box.select(v.SPACE, v.INDEX_MAIN, file_hash, part_number, quality)
-- Если он есть - возвращаем сразу
if t ~= nil then
return t
end
-- Создадим ключ, идентифицирующий запрошенный кусочек, ipc channel и запишем его
-- в таблицу для того, чтобы мы смогли получить уведомление о выполнении задания
local table_key = box.pack('ppp', file_hash, part_number, quality)
local ch = box.ipc.channel(1)
v.ctable[table_key] = ch
-- Создадим запись о кусочке в статусе «хочу конвертации»
box.insert(v.SPACE, file_hash, part_number, quality, STATUS_QUEUED)
-- Если есть ожидающие воркеры — оповестим их о появлении задания
if s.waitch:has_readers() then
s.waitch:put(true, 0)
end
-- Ожидаем выполнения задания не более timeout секунд
local body = ch:get(timeout)
if body ~= nil then
if body == false then
-- Не смогли выполнить задание, возвращаем ошибку
return box.tuple.new({RET_ERROR})
else
-- Задание выполнено, селектим результат и возвращаем
local new_tuple = box.select(v.SPACE, v.INDEX_MAIN, file_hash, part_number, quality)
return new_tuple
end
else
-- Случился таймаут ожидания, возвращаем ошибку
return box.tuple.new({RET_ERROR})
end
end
local table_key = box.pack('ppp', file_hash, part_number, quality)
v.ctable[table_key]:put(true, 0)
Реальный код чуть сложнее: например, в нем обрабатываются ситуации, когда фрагмент на момент запроса находится в статусе «в процессе конвертации». Благодаря такой схеме конвертер мгновенно узнает о появлении задания, а клиент — о завершении его выполнения, и это очень важно, ведь чем дольше пользователь видит «крутилку» загрузки видео, тем выше вероятность, что он покинет страницу, так и не дождавшись начала воспроизведения.
Как можно видеть из графика ниже, большинство конвертаций, а соответственно и ожидание пользователем воспроизведения, длится не более пары секунд.
Для конвертации мы используем модифицированный нами FFmpeg. Изначально мы хотели воспользоваться встроенными средствами FFmpeg для конвертирования в HLS, однако при ближайшем рассмотрении выяснилось, что в нашем случае с этим есть некоторые проблемы. Если попросить FFmpeg сконвертировать файл длительностью 20 секунд в HLS с 10-секундными фрагментами, то на выходе мы получим два файла и плейлист, при проигрывании которого проблем не возникает. Но если попросить его сконвертировать тот же файл сначала с 0 по 10 секунду, а потом (отдельным запуском FFmpeg) с 10 по 20 секунду, и сделать правильный плейлист, то при переходе с одного файла на другой (примерно на 10-й секунде) мы услышим заметный звуковой щелчок. Мы потратили не один день на перебор различных параметров запуска FFmpeg, но ни к какому результату не пришли. Пришлось влезть внутрь и написать небольшой патч, который при передаче определенного параметра командной строки исправляет этот недочет, возникающий из-за особенностей кодирования аудио- и видеодорожек.
Кроме того, мы использовали и некоторые другие доступные патчи, которые не были включены в FFmpeg на тот момент — например, патч для решения известной проблемы с очень медленной конвертацией MOV-файлов (видео, снятое на iPhone). Получением заданий из базы и запуском FFmpeg управляет демон по имени Aurora, который, как и демон, стоящий по другую сторону базы, написан на языке Perl и работает асинхронно с применением event-loop’a EV и различных полезных модулей, например, таких, как EV-Tarantool и Async: Chain.
Интересной особенностью запуска видео в Облаке Mail.Ru является то, что для этого не было установлено ни одного дополнительного сервера — самая требовательная к ресурсам часть (конвертация) работает на наших стораджах в специальной изолированной среде. Логи и графики показывают, что мы без проблем можем обрабатывать нагрузку, в разы превышающую уже имеющуюся. Для справки: с момента запуска в конце июня 2015 г. у нас запросили более 5 млн уникальных видео, а в минуту просматривается 500–600 уникальных файлов.
Сейчас чуть ли не каждый имеет смартфон, а то и два. Съемка коротких видео для последующего показа друзьям и близким давно в порядке вещей. Поэтому мы предусмотрели сценарий, когда человек заливает в Облако видео со смартфона или планшета, а потом сразу удаляет с мобильного устройства, чтобы освободить место в памяти. Если пользователь хочет это видео кому-нибудь показать, он может просто открыть его прямо в мобильном приложении Облака Mail.Ru или запустить проигрыватель в веб-версии Облака на десктопе. В результате стало возможным не хранить на своем смартфоне множество снятых коротких видео, при этом всегда иметь к ним доступ с любого устройства. В режиме мобильного интернета уменьшается битрейт, а соответственно и размер в мегабайтах.
Кроме того, при проигрывании на мобильных платформах мы задействуем нативные библиотеки Android и iOS. Поэтому видео проигрывается на смартфонах и планшетах «из коробки», в мобильных браузерах: для используемого нами формата не нужно разрабатывать дополнительные проигрыватели. Как и в случае с десктопными ОС, при необходимости задействуется адаптивный механизм: качество изображения динамически подстраивается под текущую пропускную способность канала.
Одним из основных отличий нашего плеера от «конкурентов» является его независимость от используемой среды. В большинстве случаев разработчики делают сразу два разных плеера: первый — с интерфейсом на Flash, второй (для браузеров, которые нативно поддерживают HLS, например, Safari) — точно такой же, но на HTML5, с подгрузкой соответствующего интерфейса. У нас же плеер один. Создавая его, мы добивались того, чтобы у нас была возможность без особых усилий поменять интерфейс. Поэтому для видео и аудио он практически одинаков — все иконки, верстка и т.д. полностью написаны на HTML5. Плеер не зависит от технологии, в которой мы показываем видео.
Flash мы используем в качестве средства отрисовки, которое только показывает видео, а весь интерфейс построен на HTML, в связи с чем мы не сталкиваемся с проблемой рассинхронизации версий, так как отсутствует необходимость поддержания Flash-версии. Для проигрывания HLS достаточно было opensource-библиотеки. Чтобы обеспечить ее работу, мы с нуля написали реализацию интерфейса элемента (которая соответствует интерфейсу видеоэлемента из стандартного HTML5), вызовы функций которого просто «транслируем» во flash-библиотеку. Поэтому всю интерфейсную часть мы пишем исходя из того, что всегда работаем с HTML5-элементом видео и следуем его стандарту. Если же в браузере нет поддержки этого формата, то мы просто подменяем нативный элемент видео на наш собственный, который реализует тот же самый интерфейс.
Если же у пользователя не поддерживается Flash, то видео воспроизводится в HTML5 с поддержкой HLS (пока это реализовано только в Safari). На Android 4.2+ и iOS HLS воспроизводится нативными средствами. При отсутствии поддержки и нативного формата, мы предлагаем пользователю скачать файл.
Если у вас был опыт реализации воспроизведения видео, приглашаем вас в комментарии: интересно, как вы решали для себя вопрос с разбивкой видео на фрагменты, как выбирали между хранением и кэшированием, с чем еще пришлось столкнуться. В общем, давайте делиться опытом.