Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Мы в Одноклассниках любим радовать пользователей, и на Новый 2021 год подготовили для самых активных из них 40 миллионов персонализированных поздравительных роликов с друзьями в главных ролях. Чтобы это удалось, понадобился творческий подход и слаженная работа сразу нескольких команд: дизайна, платформы рекомендаций и платформенной разработки видео.
В этой статье расскажем, как совершенствуем наш конвейер по генерации видео для того, чтобы справляться с пиковой новогодней нагрузкой. Покажем вклад каждого направления и проследим весь путь создания любого поздравительного видео в ОК — не только новогоднего, но и на день рождения, годовщину в ОК и на восьмое марта.
Хотя речь в большей степени пойдет про технические задачи, начать надо с самой творческой составляющей — с идеи. Потому что именно от неё зависит, какие решения нам понадобится реализовать.
Главная цель поздравительного ролика — порадовать пользователей (а вот измерять, насколько это получилось, мы уже можем через просмотры, лайки, публикации в ленте). Поэтому решили заранее попросить помощи у нашей аудитории и узнать, какие из идей им больше нравятся. А для этого в свою очередь нужно проработать сразу несколько вариантов сюжетов.
Работа над сценарием начинается с мозгового штурма, в котором участвуют команда дизайна и несколько креативных коллег из других направлений, которым интересно повлиять на создание такого массового проекта. Благодаря тому, что к генерированию идей присоединяются разные люди, рождается много классных вариантов и в то же время отсеиваются потенциально чувствительные темы. В этом году за трёхчасовой мозговой штурм коллеги придумали 10 видов сценариев.
Дальше для каждого из этих 10 черновых сценариев создаётся примерная раскадровка и к процессу подключаются пользователи: фокус-группа оценивает наброски. Они голосуют за понравившиеся варианты, делятся своими сомнениями и подкидывают идеи для развития сюжета.
После сбора обратной связи и выбора фаворита начинается детальная работа над анимацией. Команда дизайна прорабатывает подробности и ищет идеальный вариант, который будет и милым, и забавным, и достаточно универсальным, чтобы понравиться огромному числу наших пользователей с абсолютно разными вкусами. На этом этапе дизайнеры прорисовывают все кадры будущего ролика, оставляя и размечая места для фотографий и текста.
При этом, конечно, в ход идут небольшие хитрости, например, персонажи одеты в унисекс-костюмы, чтобы нам делать меньше вариаций и быстрее производить ролики. Унификация помогает сэкономить на длительности рендера за счёт того, что все необходимые ресурсы (фоны, фразы и другие неизменные изображения) вмещаются в кеш в оперативной памяти и за ними не нужно ходить на медленный жесткий диск. Кроме того, обязательно готовим красивую обложку с портретом пользователя-адресата и вводим его персонажа с первых секунд, чтобы цеплять при включенном режиме автовоспроизведения и повысить шансы на репост ролика. А еще недавно мы узнали, что пользователи хотят видеть в поздравительных видео больше лиц друзей, поэтому в новогоднем ролике у нас впервые так много персонажей. Выбрать, кто из друзей будет на их месте, помогает команда платформы рекомендаций.
Параллельно с подготовкой шаблона ролика нужно решить две задачи: понять, кого будем поздравлять, и отобрать для каждого получателя поздравления друзей, которые появятся в видео.
У Одноклассников миллионы пользователей и, к сожалению, подготовить именно новогодние ролики вообще для всех мы не можем — нужно слишком много ресурсов на рендеринг, чтобы успеть за время праздников. Поэтому ищем баланс и делаем видео в первую очередь для самых активных пользователей, которые чаще всего заходят в соцсеть и получают больше всего обратной связи о своих публикациях. Идея понятна: они с большей вероятностью увидят видео и поделятся им, а когда поделятся, то соберут больше просмотров и лайков. Мы как заправские инфлюэнсеры жадны до просмотров. :)
Команда платформы рекомендаций собирает некий агрегированный показатель активности для каждого пользователя, сортирует список, и для верхней половины или трети (сколько планируем успеть сделать роликов) ищет героев поздравительного мультфильма. То есть из все знакомых выбирает тех друзей пользователя, появление которых больше всего порадует.
У нас нет точных данных о том, кого бы пользователь хотел увидеть в ролике (но мы планируем начать собирать такую информацию). Приходится строить эвристическую оценку близкой дружбы и предсказывать её на основе признаков друзей:
На основе значений этих признаков обучается ранжирующая модель XGBoost на Hadoop.
В результате для каждого пользователя из целевого списка мы получаем список друзей, отсортированный в порядке убывания значения ранжирующей функции. Топ активных пользователей отправляется в очередь на генерирование роликов в формате
Кроме списков активных пользователей и их друзей для генерирования роликов нам понадобятся их портреты. Причем это должны быть именно лица, не слишком мелкие и расположенные более-менее прямо — короче, почти как на паспорт. А фотографии на аватарках у людей самые разные, поэтому за подходящими изображениями будем обращаться к специальному сервису, который занимается подготовкой портретов пользователей.
Портретный сервис действует следующим образом:
Раньше, когда готовили первый поздравительный ролик на день рождения, после определения ключевых точек лица был еще один шаг: нейросетью отделяли пиксели портрета от пикселей фона. Делали это, чтобы реалистично вставлять лицо пользователя в видео. Но потом стали делать милые мультяшные ролики и необходимость в точной обтравке лица отпала — овал лучше вписывается в выбранную стилистику и к тому же позволяет экономить время и ресурсы.
Если же пользователь тщательно скрывается от камер и сервис подготовки портретов не может найти ни одной подходящей фотографии, то в адресованном ему поздравительном ролике будет картинка-заглушка. А на главные роли мы постараемся подобрать друзей, у которых фотографии всё же есть. Но если это не получится, то некоторые роли достанутся массовке и вместо лиц друзей в ролике будут анимированные персонажи. Так что выглядеть это будет максимально естественно, просто менее значительные по сюжету персонажи буду с заглушками, а близкие друзья всё равно поедут вместе на ёлку.
Итак, к этому моменту у нас есть:
Остаётся собрать всё это вместе и запустить конвейер по производству десятков миллионов видео.
Уточним, что мы делаем новогодние ролики не на лету и не по запросу. Как эльфы Санта-Клауса готовим подарки заранее: в начале декабря выделяем порядка тысячи серверов, запускаем очередь и генерируем по миллиону роликов в сутки, чтобы к середине декабря накопить достаточно для массового распространения новогоднего настроения. Но и дальше генерирование роликов продолжается и кто-то получает поздравления позже. Некоторые даже успевают начать волноваться и написать в техподдержку:
Для того, чтобы запустить потоковое генерирование видео, нужно задать конфигурацию: объяснить генератору, из каких исходников собирать кадры. Каждый кадр, как бутерброд, состоит из нескольких слоёв: портрета или портретов пользователей, текста и мультяшной иллюстрации сверху. В конфигурационном файле наши инженеры скрупулёзно прописывают необходимые для правильного расположения всех элементов параметры:
То есть уже на этом уровне прописывается логика относительного того, какие именно слои использовать в данном конкретном случае в данном конкретном кадре.
У слоя, отвечающего за портрет пользователя, сейчас появилось два важных улучшения. Чтобы их лучше понять, немного углубимся в то, как именно подставляется портрет — изображение пользователя с координатами точек лица. В конфигураторе нам нужно выбрать место, масштаб и угол поворота для подстановки изображения. Все три параметра команда дизайнеров «кодировала» в специальном маркере. Маркер — это одноцветный квадрат, через его центр определяется положение точки между глаз на портрете, размер задает масштаб, а угол поворота показывает наклон головы персонажа.
В прошлых версиях конфигуратора, когда было нужно отрисовать персонажа номер 4 на 124 кадре, мы загружали изображение маркера четвертого персонажа с номером 124 и по нему находили все три характеристики. Поэтому первым улучшением стало использование заранее посчитанных характеристик маркеров вместо вычисления их при генерировании каждого ролика. Получилось избавиться и от лишних чтений файлов (ведь теперь в изображениях маркеров отпала необходимость), и от лишних вычислений, связанных с поиском маркера на изображении и расчетом его характеристик. Это ускорило отрисовку кадров ролика более, чем в два раза.
Второе улучшение связано с тем, как мы определяем угол поворота портрета. Если раньше мы просто брали угол поворота маркера, то теперь добавляем к нему угол между горизонталью и прямой, проведенной через зрачки. Так мы учитываем, что на портрете голова уже может быть наклонена. На примере видно, что лицо теперь лучше вписывается в анимацию.
А ещё к последнему новогоднему ролику мы существенно упростили себе отладку конфигурации. Раньше его нужно было полностью прописать и отправить запрос на сервер, чтобы тот сгенерировал по конфигурации видео. Это было не очень удобно, потому что первый блин всегда комом: то маска не применилась, то слои местами перепутаны, и т.д. А чтобы это увидеть, приходилось ждать, пока отрендерится весь ролик.
Теперь у нас есть специальный редактор, который работает локально. Это десктопное приложение, написанное для внутренних нужд команд, занимающихся поздравительными роликами: дизайнеров и программистов. Оно помогает ускорить превращение ресурсов, созданных командой дизайна, в ролик, поставленный на поток. Самая полезная часть редактора — мгновенное конфигурирование и рендер кадра по номеру. Без редактора приходилось настраивать всё на удаленном сервере, загружать на него ресурсы и ждать несколько минут, пока соберется весь ролик. А теперь можно быстро проверить конкретный кадр, например, что у ролика правильная обложка. Также редактор удобно использовать для тестов производительности, и в результате ускорять процесс генерирования роликов.
Когда отладили конфигурирование, можно переходить к массовому производству и запускать очереди.
Каждый ролик проходит через две очереди: в одной расставляются приоритеты между типами роликов, вторая определяет порядок рендеринга. Обе очереди у нас самописные, о том, как они устроены, можно посмотреть в докладе о платформе видео, а здесь обсудим их назначение и верхнеуровневую организацию.
Первая очередь, — назовём её memories-очередь, — индивидуальная для каждого типа ролика (на день рождения, новый год и т.д.). Она нужна для приоритизации типов роликов: например, у видео на день рождения приоритет выше, чем у новогоднего, потому что подарок на день рождения важнее доставить вовремя. Очередь на генерирование новогодних роликов достигала 7 миллионов пользователей, и если бы она была выше по приоритету, то с нашей скоростью обработки в 1 миллион роликов в сутки поздравление с днём рождения опоздало бы как минимум на неделю, что нам никак не подходит.
Приоритеты между memories-очередями расставляются с помощью второй очереди — transform-очереди на генерирование видео. В transform-очередь рано или поздно попадают все ролики, но в каждый момент времени в ней не может быть больше роликов из одной memories-очереди, чем заданное пороговое значение. Например, размер transform-очереди 10000, порог для memories-очереди с роликами на день рождения равен 1000, для новогодних роликов — 500. Пока в transform-очереди будет меньше 500 новогодних роликов, в неё будут добавляться ролики обоих типов. Но как только новогодних роликов в transform-очереди станет 500, она начнёт заполняться исключительно роликами других типов, например, на день рождения. Когда задачи на ролики на день рождения закончатся, снова начнут исполняться задачи на новогодние ролики и очередь будет оперативно ими пополняться. Пороговые значения мы можем менять в любой момент времени, так что нам легко контролировать, какие ролики сейчас генерируются.
Transform-очередь одна для всех типов поздравительных роликов и заполняется самыми высокоприоритетными роликами для генерирования. Но в неё же попадают и другие задачи, например, обработка пользовательского видео, миграция файлов или импорт данных от поставщиков. У этих типов задач тоже есть свои приоритеты, и transform-очередь учитывает их все.
Каждый transform-сервер берет список задач, которые он может выполнять, сортирует по важности, проверяет самую важную очередь, и если в ней есть задача, то выполняет ее, а если нет — проверяет менее важную, и так далее. Список задач у разных transform-серверов отличается, не все из них универсальные, потому что, например, для генерирования поздравительных роликов в локальном хранилище сервера должны храниться исходники для ролика, а качать их на все наши transform-серверы не имеет смысла.
Списки задач transform-серверов мы тоже можем менять когда захотим. Если, допустим, удалось «одолжить» 500 серверов для ускорения генерирования новогодних роликов, то мы этим серверам настраиваем список задач таким образом, чтобы у новогодних роликов был высший приоритет. Но вообще мы оптимизировали выполнение самих задач по нарезке видео, чтобы «занимать» серверы не приходилось.
Из transform-очереди запускаются задачи по сборке ролика и его нарезке в нужные форматы и разрешения. Первая версия конвейера генерирования видео выглядела примерно так:
Это всё работало, но недостаточно быстро — подготовить запланированное количество роликов к сроку мы бы не успели, а Новый год не тот дедлайн, который можно подвинуть. Мы не хотели оставлять пользователей без подарков, поэтому стали искать узкие места.
Три основных шага конвейера: подготовка кадров, сборка из них видео, транскодирование в разные форматы (последний шаг, конечно, распараллеливался, это часть нашего стандартного конвейера нарезки видео). В первой версии они занимали примерно одинаковое время, и мы попробовали ускорить каждый из них.
Для начала положили все ресурсы в оперативную память, но существенного прироста не получили. Оказалось, что основное время тратилось не на чтение с диска, а на декодирование изображения из png в BufferedImage. Тогда стали хранить в памяти уже готовые BuffereImage, для чего пришлось повозиться с тем, чтобы аккуратно избежать все их изменения.
После этого стало очевидно, что лишнее время уходит на работу с портретами. Поменяли работу с ними так, чтобы подготавливать портрет пользователя один раз на стороне портретного сервиса и сразу в нужном виде, и если портрет понадобится в другом ролике, то использовать готовую версию из кеша. Заодно убрали, где было возможно, лишнее копирование изображений.
Второй и третий шаг оптимизировали одним махом. Вместо того, чтобы собирать jpeg в видео и потом перекодировать его в разные расширения, мы используем
Эти небольшие трюки помогли суммарно сократить время на подготовку одного ролика примерно в два раза. Что в свою очередь позволило добиться того самого миллиона роликов в сутки. Причём нарезка видео и подготовка портретов расположены в группе фоновых задач нашего облака (подробнее, как оно устроено, рассказано в другой статье), поэтому мы, с одной стороны, используем под них максимальное количество свободных ресурсов, а с другой — не мешаем обработке пользовательских запросов.
Когда, казалось бы, всё готово и процессы генерирования роликов запущены на тысячах серверов, возникает следующая техническая задача: показать эти миллионы роликов в ленте. Момента тут три: выдержать пиковую нагрузку при раздаче видео; донести до адресата и его друзей поздравление; не завалить вообще всю ленту похожим контентом и не расстроить тех, кому это не очень интересно.
Первая трудность связана с тем, что пользователи начинают смотреть и запрашивать с серверов новогодние ролики практически одновременно. Для быстрой доставки содержимого у нас, конечно, есть CDN. Популярные данные попадают в кеши центральных серверов раздачи и CDN и раздаются с серверов, расположенных ближе к пользователю. Но когда надо показать каждому из десятков миллионов пользователей свой уникальный ролик, кеширование не помогает и бутылочным горлышком, как ни странно, становится скорость чтения с дисков внутреннего хранилища видео (как устроено хранилище, смотрите в этом видео). Серверы раздачи содержимого пытаются скопировать себе миллионы роликов с центрального сервера, но он чисто физически не успевает их отдавать. В итоге может пострадать доставка вообще всех данных, не только поздравлений.
Для решения проблемы пиковой нагрузки на серверы раздачи мы применяем превентивные меры: начинаем показывать ролики только части пользователей, чтобы скачивать их постепенно. Если по графикам мониторинга видно, что сервера не справляются, то еще уменьшаем долю новых получателей поздравлений. На те ролики, которые уже опубликованы, мы никак не влияем, но можем показывать меньше новых, чтобы их меньше публиковали и нам нужно было скачивать меньше копий. Благодаря этому Одноклассники в сумме показали новогодние ролики более 2,5 миллиард раз и успешно пережили рекорд в 353 миллиона просмотров за сутки.
Дальше мы помогаем не пропустить видео с помощью интерфейсных решений: заранее предупреждаем, что скоро появится поздравление; показываем уведомление, когда видео готово; предлагаем поделиться; автоматически отмечаем друзей и даже секунду, на которой они первый раз «попадают в кадр».
Обычно 30-40 % людей, получивших видео, делятся им в ленте. Но иногда что-то идёт не так и на помощь опять приходит техподдержка.
Конечно, мы не удаляем видео сразу и с удовольствием помогаем и возвращаем потерянные ролики.
Для сохранения разнообразия постов в ленте, во-первых, стараемся разбавлять посты с новогодними роликами другими новостями, во-вторых, схлопываем события с заметкой и отметкой на видео. Обычно это два события, но чтобы не переборщить с похожими постами, жертвуем комментарием в заметке в пользу просмотров, которых больше у записей с отметками друзей. Ничего особо хитрого, просто сто первый нюанс, который нужно учесть.
В результате изменения работы с конфигуратором и оптимизации конвейера генерирования роликов к 2021 Новому году мы подготовили в два раза больше персонализированных поздравительных роликов, чем в прошлом году. Среднее количество просмотров и классов на одном ролике тоже выросло в два раза — и это, как нам кажется, заслуга всех участвующих команд. И всем нам приятно видеть не только сводную статистику, но и живые отзывы. Например, такие:
«Уважаемые ОК! Спасибо Вам большое за видеоролик к Новому году с участием моих друзей. Очень приятно и неожиданно. Некоторые думают, что это я, сама всё создала и выставила. У меня к вам секретная просьба: подготовить подобный ролик для *** с моим участием и, конечно, с другими друзьями. Он как раз и не верит, что это вы такие молодцы.»
А еще отзывы помогают понять, что улучшать. В ближайших планах:
Как видите, в каждый минутный поздравительный ролик в ОК разные специалисты вкладывают много усилий и делают это сообща. У нас есть общая цель и поэтому технические трудности для нас вызовы, а не препятствия.
P.S.: Над этой статьей, как и над поздравительными роликами, работали представители разных команд. Спасибо за помощь Ярославу Иванову, Алексею Черепененкову, Александру Дзюбе, Наталье Толстовой, Екатерине Ямщиковой, Алексею Яничкину, Олегу Ларионову, Евгении Финкельштейн.
В этой статье расскажем, как совершенствуем наш конвейер по генерации видео для того, чтобы справляться с пиковой новогодней нагрузкой. Покажем вклад каждого направления и проследим весь путь создания любого поздравительного видео в ОК — не только новогоднего, но и на день рождения, годовщину в ОК и на восьмое марта.
Хотя речь в большей степени пойдет про технические задачи, начать надо с самой творческой составляющей — с идеи. Потому что именно от неё зависит, какие решения нам понадобится реализовать.
Идея и сценарий
Главная цель поздравительного ролика — порадовать пользователей (а вот измерять, насколько это получилось, мы уже можем через просмотры, лайки, публикации в ленте). Поэтому решили заранее попросить помощи у нашей аудитории и узнать, какие из идей им больше нравятся. А для этого в свою очередь нужно проработать сразу несколько вариантов сюжетов.
Работа над сценарием начинается с мозгового штурма, в котором участвуют команда дизайна и несколько креативных коллег из других направлений, которым интересно повлиять на создание такого массового проекта. Благодаря тому, что к генерированию идей присоединяются разные люди, рождается много классных вариантов и в то же время отсеиваются потенциально чувствительные темы. В этом году за трёхчасовой мозговой штурм коллеги придумали 10 видов сценариев.
Дальше для каждого из этих 10 черновых сценариев создаётся примерная раскадровка и к процессу подключаются пользователи: фокус-группа оценивает наброски. Они голосуют за понравившиеся варианты, делятся своими сомнениями и подкидывают идеи для развития сюжета.
После сбора обратной связи и выбора фаворита начинается детальная работа над анимацией. Команда дизайна прорабатывает подробности и ищет идеальный вариант, который будет и милым, и забавным, и достаточно универсальным, чтобы понравиться огромному числу наших пользователей с абсолютно разными вкусами. На этом этапе дизайнеры прорисовывают все кадры будущего ролика, оставляя и размечая места для фотографий и текста.
При этом, конечно, в ход идут небольшие хитрости, например, персонажи одеты в унисекс-костюмы, чтобы нам делать меньше вариаций и быстрее производить ролики. Унификация помогает сэкономить на длительности рендера за счёт того, что все необходимые ресурсы (фоны, фразы и другие неизменные изображения) вмещаются в кеш в оперативной памяти и за ними не нужно ходить на медленный жесткий диск. Кроме того, обязательно готовим красивую обложку с портретом пользователя-адресата и вводим его персонажа с первых секунд, чтобы цеплять при включенном режиме автовоспроизведения и повысить шансы на репост ролика. А еще недавно мы узнали, что пользователи хотят видеть в поздравительных видео больше лиц друзей, поэтому в новогоднем ролике у нас впервые так много персонажей. Выбрать, кто из друзей будет на их месте, помогает команда платформы рекомендаций.
Кастинг на главные роли
Параллельно с подготовкой шаблона ролика нужно решить две задачи: понять, кого будем поздравлять, и отобрать для каждого получателя поздравления друзей, которые появятся в видео.
У Одноклассников миллионы пользователей и, к сожалению, подготовить именно новогодние ролики вообще для всех мы не можем — нужно слишком много ресурсов на рендеринг, чтобы успеть за время праздников. Поэтому ищем баланс и делаем видео в первую очередь для самых активных пользователей, которые чаще всего заходят в соцсеть и получают больше всего обратной связи о своих публикациях. Идея понятна: они с большей вероятностью увидят видео и поделятся им, а когда поделятся, то соберут больше просмотров и лайков. Мы как заправские инфлюэнсеры жадны до просмотров. :)
Команда платформы рекомендаций собирает некий агрегированный показатель активности для каждого пользователя, сортирует список, и для верхней половины или трети (сколько планируем успеть сделать роликов) ищет героев поздравительного мультфильма. То есть из все знакомых выбирает тех друзей пользователя, появление которых больше всего порадует.
У нас нет точных данных о том, кого бы пользователь хотел увидеть в ролике (но мы планируем начать собирать такую информацию). Приходится строить эвристическую оценку близкой дружбы и предсказывать её на основе признаков друзей:
- категории, которые указал в соцграфе сам пользователь («лучшие друзья», «коллеги», «родственники»);
- счетчики взаимодействия, которые используются в ленте и других сервисах, такие как число взаимных сообщений, подарков, походов в «гости», отметок на фото и пр.;
- информация, указанная в профиле.
На основе значений этих признаков обучается ранжирующая модель XGBoost на Hadoop.
В результате для каждого пользователя из целевого списка мы получаем список друзей, отсортированный в порядке убывания значения ранжирующей функции. Топ активных пользователей отправляется в очередь на генерирование роликов в формате
<пользователь, сортированный список друзей>
. Классные портреты для персонажей
Кроме списков активных пользователей и их друзей для генерирования роликов нам понадобятся их портреты. Причем это должны быть именно лица, не слишком мелкие и расположенные более-менее прямо — короче, почти как на паспорт. А фотографии на аватарках у людей самые разные, поэтому за подходящими изображениями будем обращаться к специальному сервису, который занимается подготовкой портретов пользователей.
Портретный сервис действует следующим образом:
- Проверяет, есть ли подходящий портрет в кеше. Если нет, то переходит к следующим шагам.
- Среди общедоступных фотографий пользователя с помощью нейросети ищет, где есть его лицо (о том, как у нас устроено распознавание лиц, читайте в этой статье). На этом этапе расположение лица определяется приблизительно, главное, что оно есть и принадлежит кому надо.
- Из найденных фотографий с лицом пользователя сервис оставляет только те, где лицо достаточно крупное, чтобы для ролика его не пришлось слишком сильно растягивать.
- На отобранных крупных фотографиях с помощью библиотеки dlib находит ключевые точки, которые помогут правильно расположить портрет в ролике. На этом же шаге отфильтровываются все фотографии с лицом в профиль или вполоборота — они нам не подойдут.
- После этого сервис вырезает фрагмент фотографии с лицом и складывает в кеш, который используется для изображений определённого типа и недоступен извне. В конец сохранённого изображения дописывается информация о точках лица, использующаяся при сборке кадров, — к счастью, формат png позволяет дописывать произвольные данные после IEND.
Раньше, когда готовили первый поздравительный ролик на день рождения, после определения ключевых точек лица был еще один шаг: нейросетью отделяли пиксели портрета от пикселей фона. Делали это, чтобы реалистично вставлять лицо пользователя в видео. Но потом стали делать милые мультяшные ролики и необходимость в точной обтравке лица отпала — овал лучше вписывается в выбранную стилистику и к тому же позволяет экономить время и ресурсы.
Если же пользователь тщательно скрывается от камер и сервис подготовки портретов не может найти ни одной подходящей фотографии, то в адресованном ему поздравительном ролике будет картинка-заглушка. А на главные роли мы постараемся подобрать друзей, у которых фотографии всё же есть. Но если это не получится, то некоторые роли достанутся массовке и вместо лиц друзей в ролике будут анимированные персонажи. Так что выглядеть это будет максимально естественно, просто менее значительные по сюжету персонажи буду с заглушками, а близкие друзья всё равно поедут вместе на ёлку.
Камера, мотор!
Итак, к этому моменту у нас есть:
- подготовленный для персонализации мультик, т.е. набор кадров с «вырезами» под лица и разметкой для текста;
- целевой список пользователей, которых хотим поздравить, и отсортированные списки их друзей, которые должны появиться в ролике;
- сервис, который по запросу подготавливает портреты всех персонажей.
Остаётся собрать всё это вместе и запустить конвейер по производству десятков миллионов видео.
Уточним, что мы делаем новогодние ролики не на лету и не по запросу. Как эльфы Санта-Клауса готовим подарки заранее: в начале декабря выделяем порядка тысячи серверов, запускаем очередь и генерируем по миллиону роликов в сутки, чтобы к середине декабря накопить достаточно для массового распространения новогоднего настроения. Но и дальше генерирование роликов продолжается и кто-то получает поздравления позже. Некоторые даже успевают начать волноваться и написать в техподдержку:
Добрый день. К новому году у многих появилось видео от одноклассников. Как я могу его получить?
У друзей появилось видео С новым годом! от ОК, а у меня нет ссылки на него, я тоже хочу
Помогите, пришлите ССЫЛКУ
Для того, чтобы запустить потоковое генерирование видео, нужно задать конфигурацию: объяснить генератору, из каких исходников собирать кадры. Каждый кадр, как бутерброд, состоит из нескольких слоёв: портрета или портретов пользователей, текста и мультяшной иллюстрации сверху. В конфигурационном файле наши инженеры скрупулёзно прописывают необходимые для правильного расположения всех элементов параметры:
- где брать исходники;
- кто должен появиться в кадре — какой по счету друг из топа и что делать, если нет его портрета;
- куда подставлять портрет, как его расположить, используя маркеры глаз и другие ключевые точки;
- где должна появиться надпись с репликой героя, что должно быть написано с учётом локализации на 15 языков;
То есть уже на этом уровне прописывается логика относительного того, какие именно слои использовать в данном конкретном случае в данном конкретном кадре.
У слоя, отвечающего за портрет пользователя, сейчас появилось два важных улучшения. Чтобы их лучше понять, немного углубимся в то, как именно подставляется портрет — изображение пользователя с координатами точек лица. В конфигураторе нам нужно выбрать место, масштаб и угол поворота для подстановки изображения. Все три параметра команда дизайнеров «кодировала» в специальном маркере. Маркер — это одноцветный квадрат, через его центр определяется положение точки между глаз на портрете, размер задает масштаб, а угол поворота показывает наклон головы персонажа.
В прошлых версиях конфигуратора, когда было нужно отрисовать персонажа номер 4 на 124 кадре, мы загружали изображение маркера четвертого персонажа с номером 124 и по нему находили все три характеристики. Поэтому первым улучшением стало использование заранее посчитанных характеристик маркеров вместо вычисления их при генерировании каждого ролика. Получилось избавиться и от лишних чтений файлов (ведь теперь в изображениях маркеров отпала необходимость), и от лишних вычислений, связанных с поиском маркера на изображении и расчетом его характеристик. Это ускорило отрисовку кадров ролика более, чем в два раза.
Второе улучшение связано с тем, как мы определяем угол поворота портрета. Если раньше мы просто брали угол поворота маркера, то теперь добавляем к нему угол между горизонталью и прямой, проведенной через зрачки. Так мы учитываем, что на портрете голова уже может быть наклонена. На примере видно, что лицо теперь лучше вписывается в анимацию.
А ещё к последнему новогоднему ролику мы существенно упростили себе отладку конфигурации. Раньше его нужно было полностью прописать и отправить запрос на сервер, чтобы тот сгенерировал по конфигурации видео. Это было не очень удобно, потому что первый блин всегда комом: то маска не применилась, то слои местами перепутаны, и т.д. А чтобы это увидеть, приходилось ждать, пока отрендерится весь ролик.
Теперь у нас есть специальный редактор, который работает локально. Это десктопное приложение, написанное для внутренних нужд команд, занимающихся поздравительными роликами: дизайнеров и программистов. Оно помогает ускорить превращение ресурсов, созданных командой дизайна, в ролик, поставленный на поток. Самая полезная часть редактора — мгновенное конфигурирование и рендер кадра по номеру. Без редактора приходилось настраивать всё на удаленном сервере, загружать на него ресурсы и ждать несколько минут, пока соберется весь ролик. А теперь можно быстро проверить конкретный кадр, например, что у ролика правильная обложка. Также редактор удобно использовать для тестов производительности, и в результате ускорять процесс генерирования роликов.
Когда отладили конфигурирование, можно переходить к массовому производству и запускать очереди.
В очередь на трансформацию становись
Каждый ролик проходит через две очереди: в одной расставляются приоритеты между типами роликов, вторая определяет порядок рендеринга. Обе очереди у нас самописные, о том, как они устроены, можно посмотреть в докладе о платформе видео, а здесь обсудим их назначение и верхнеуровневую организацию.
Первая очередь, — назовём её memories-очередь, — индивидуальная для каждого типа ролика (на день рождения, новый год и т.д.). Она нужна для приоритизации типов роликов: например, у видео на день рождения приоритет выше, чем у новогоднего, потому что подарок на день рождения важнее доставить вовремя. Очередь на генерирование новогодних роликов достигала 7 миллионов пользователей, и если бы она была выше по приоритету, то с нашей скоростью обработки в 1 миллион роликов в сутки поздравление с днём рождения опоздало бы как минимум на неделю, что нам никак не подходит.
Приоритеты между memories-очередями расставляются с помощью второй очереди — transform-очереди на генерирование видео. В transform-очередь рано или поздно попадают все ролики, но в каждый момент времени в ней не может быть больше роликов из одной memories-очереди, чем заданное пороговое значение. Например, размер transform-очереди 10000, порог для memories-очереди с роликами на день рождения равен 1000, для новогодних роликов — 500. Пока в transform-очереди будет меньше 500 новогодних роликов, в неё будут добавляться ролики обоих типов. Но как только новогодних роликов в transform-очереди станет 500, она начнёт заполняться исключительно роликами других типов, например, на день рождения. Когда задачи на ролики на день рождения закончатся, снова начнут исполняться задачи на новогодние ролики и очередь будет оперативно ими пополняться. Пороговые значения мы можем менять в любой момент времени, так что нам легко контролировать, какие ролики сейчас генерируются.
Transform-очередь одна для всех типов поздравительных роликов и заполняется самыми высокоприоритетными роликами для генерирования. Но в неё же попадают и другие задачи, например, обработка пользовательского видео, миграция файлов или импорт данных от поставщиков. У этих типов задач тоже есть свои приоритеты, и transform-очередь учитывает их все.
Каждый transform-сервер берет список задач, которые он может выполнять, сортирует по важности, проверяет самую важную очередь, и если в ней есть задача, то выполняет ее, а если нет — проверяет менее важную, и так далее. Список задач у разных transform-серверов отличается, не все из них универсальные, потому что, например, для генерирования поздравительных роликов в локальном хранилище сервера должны храниться исходники для ролика, а качать их на все наши transform-серверы не имеет смысла.
Списки задач transform-серверов мы тоже можем менять когда захотим. Если, допустим, удалось «одолжить» 500 серверов для ускорения генерирования новогодних роликов, то мы этим серверам настраиваем список задач таким образом, чтобы у новогодних роликов был высший приоритет. Но вообще мы оптимизировали выполнение самих задач по нарезке видео, чтобы «занимать» серверы не приходилось.
Торопимся к Новому году
Из transform-очереди запускаются задачи по сборке ролика и его нарезке в нужные форматы и разрешения. Первая версия конвейера генерирования видео выглядела примерно так:
- достаем из очереди данные для создания ролика (ID пользователя, список друзей, название и т.д.);
- читаем ресурсы с диска, подгружаем портреты пользователей;
- собираем кадры будущего ролика из портретов и ресурсов, добавляем эффекты, тексты и пр.;
- из тысячи кадров собираем видео, нарезаем его во все поддерживаемые качества и форматы (MP4, HLS, DASH для разных платформ в разрешении от превью в 256x144 до FullHD);
- сохраняем результат во внутреннем хранилище для видео, теперь ролик готов для показа на всех клиентах.
Это всё работало, но недостаточно быстро — подготовить запланированное количество роликов к сроку мы бы не успели, а Новый год не тот дедлайн, который можно подвинуть. Мы не хотели оставлять пользователей без подарков, поэтому стали искать узкие места.
Три основных шага конвейера: подготовка кадров, сборка из них видео, транскодирование в разные форматы (последний шаг, конечно, распараллеливался, это часть нашего стандартного конвейера нарезки видео). В первой версии они занимали примерно одинаковое время, и мы попробовали ускорить каждый из них.
Для начала положили все ресурсы в оперативную память, но существенного прироста не получили. Оказалось, что основное время тратилось не на чтение с диска, а на декодирование изображения из png в BufferedImage. Тогда стали хранить в памяти уже готовые BuffereImage, для чего пришлось повозиться с тем, чтобы аккуратно избежать все их изменения.
После этого стало очевидно, что лишнее время уходит на работу с портретами. Поменяли работу с ними так, чтобы подготавливать портрет пользователя один раз на стороне портретного сервиса и сразу в нужном виде, и если портрет понадобится в другом ролике, то использовать готовую версию из кеша. Заодно убрали, где было возможно, лишнее копирование изображений.
Второй и третий шаг оптимизировали одним махом. Вместо того, чтобы собирать jpeg в видео и потом перекодировать его в разные расширения, мы используем
ffmpeg -codec copy
и сразу передаём последовательность кадров на вход ffmpeg-процессу, генерирующему видео в нужных форматах. Естественно, это ускорило сборку ролика.Эти небольшие трюки помогли суммарно сократить время на подготовку одного ролика примерно в два раза. Что в свою очередь позволило добиться того самого миллиона роликов в сутки. Причём нарезка видео и подготовка портретов расположены в группе фоновых задач нашего облака (подробнее, как оно устроено, рассказано в другой статье), поэтому мы, с одной стороны, используем под них максимальное количество свободных ресурсов, а с другой — не мешаем обработке пользовательских запросов.
Запуск на большие экраны
Когда, казалось бы, всё готово и процессы генерирования роликов запущены на тысячах серверов, возникает следующая техническая задача: показать эти миллионы роликов в ленте. Момента тут три: выдержать пиковую нагрузку при раздаче видео; донести до адресата и его друзей поздравление; не завалить вообще всю ленту похожим контентом и не расстроить тех, кому это не очень интересно.
Первая трудность связана с тем, что пользователи начинают смотреть и запрашивать с серверов новогодние ролики практически одновременно. Для быстрой доставки содержимого у нас, конечно, есть CDN. Популярные данные попадают в кеши центральных серверов раздачи и CDN и раздаются с серверов, расположенных ближе к пользователю. Но когда надо показать каждому из десятков миллионов пользователей свой уникальный ролик, кеширование не помогает и бутылочным горлышком, как ни странно, становится скорость чтения с дисков внутреннего хранилища видео (как устроено хранилище, смотрите в этом видео). Серверы раздачи содержимого пытаются скопировать себе миллионы роликов с центрального сервера, но он чисто физически не успевает их отдавать. В итоге может пострадать доставка вообще всех данных, не только поздравлений.
Для решения проблемы пиковой нагрузки на серверы раздачи мы применяем превентивные меры: начинаем показывать ролики только части пользователей, чтобы скачивать их постепенно. Если по графикам мониторинга видно, что сервера не справляются, то еще уменьшаем долю новых получателей поздравлений. На те ролики, которые уже опубликованы, мы никак не влияем, но можем показывать меньше новых, чтобы их меньше публиковали и нам нужно было скачивать меньше копий. Благодаря этому Одноклассники в сумме показали новогодние ролики более 2,5 миллиард раз и успешно пережили рекорд в 353 миллиона просмотров за сутки.
Дальше мы помогаем не пропустить видео с помощью интерфейсных решений: заранее предупреждаем, что скоро появится поздравление; показываем уведомление, когда видео готово; предлагаем поделиться; автоматически отмечаем друзей и даже секунду, на которой они первый раз «попадают в кадр».
Обычно 30-40 % людей, получивших видео, делятся им в ленте. Но иногда что-то идёт не так и на помощь опять приходит техподдержка.
Администрация прислала мне Новогоднее поздравление. Я нечаянно удалила. Можно видео вернуть обратно? Пожалуйста! Спасибо!
Добрый вечер. При просмотре видеоролика «С новым годом», который вы подготовили для меня исчез интернет. Не успела сделать отметку и поделиться этим видео. Прошу вас прислать этот видеоролик ещё раз. Спасибо.
Конечно, мы не удаляем видео сразу и с удовольствием помогаем и возвращаем потерянные ролики.
Для сохранения разнообразия постов в ленте, во-первых, стараемся разбавлять посты с новогодними роликами другими новостями, во-вторых, схлопываем события с заметкой и отметкой на видео. Обычно это два события, но чтобы не переборщить с похожими постами, жертвуем комментарием в заметке в пользу просмотров, которых больше у записей с отметками друзей. Ничего особо хитрого, просто сто первый нюанс, который нужно учесть.
Как фильм восприняли зрители и когда сиквел
В результате изменения работы с конфигуратором и оптимизации конвейера генерирования роликов к 2021 Новому году мы подготовили в два раза больше персонализированных поздравительных роликов, чем в прошлом году. Среднее количество просмотров и классов на одном ролике тоже выросло в два раза — и это, как нам кажется, заслуга всех участвующих команд. И всем нам приятно видеть не только сводную статистику, но и живые отзывы. Например, такие:
«Уважаемые ОК! Спасибо Вам большое за видеоролик к Новому году с участием моих друзей. Очень приятно и неожиданно. Некоторые думают, что это я, сама всё создала и выставила. У меня к вам секретная просьба: подготовить подобный ролик для *** с моим участием и, конечно, с другими друзьями. Он как раз и не верит, что это вы такие молодцы.»
А еще отзывы помогают понять, что улучшать. В ближайших планах:
- научить сервис распознавания лиц определять улыбки и брать для портретов веселые фотографии;
- добавить простой способ поделиться роликом наружу (это и сейчас можно, но нужно копировать ссылку);
- оптимизировать рендеринг, чтобы распараллеливать его на потоки равномерно с точки зрения нагрузки;
- может быть, с помощью нейросетей добавить на фотографии мимику.
Как видите, в каждый минутный поздравительный ролик в ОК разные специалисты вкладывают много усилий и делают это сообща. У нас есть общая цель и поэтому технические трудности для нас вызовы, а не препятствия.
P.S.: Над этой статьей, как и над поздравительными роликами, работали представители разных команд. Спасибо за помощь Ярославу Иванову, Алексею Черепененкову, Александру Дзюбе, Наталье Толстовой, Екатерине Ямщиковой, Алексею Яничкину, Олегу Ларионову, Евгении Финкельштейн.