- PVSM.RU - https://www.pvsm.ru -

Мы очень любим спецпроекты. В начале декабря мы провели еще одну онлайн-игру [1], на этот раз с детектором лжи.
Это было 7-часовое онлайн-шоу, где девушек подключали к полиграфу и они отвечали на вопросы зрителей. Из комнаты велась прямая трансляция, на сайте проекта можно было отслеживать все показатели девушки во время ответа: дыхание, сердцебиение, проводимость кожи, давление и потоотделение.
Мы прикрутили к сайту геймификацию и каждый раз после ответа игроки получали 15 секунд, чтобы решить, правду говорит девушка или лжет. За правильные ответы набирались баллы.
В реализации этот проект оказался довольно сложным — рассказываем, как все было устроено с психологической и технической стороны.
Первоначальная идея игры была другой: автор оригинальной идеи предлагал дозваниваться до девушек в прямом эфире, выводить их на разговор и смотреть, как их показатели меняются. Такое полностью интерактивное шоу, где уверенные в себе молодые люди могут публично проверить свои навыки беседы и получать максимально честную обратную связь от девушек без возможности соврать.
Это был крайне ненадежный сценарий, и я объясню почему.
Есть известный принцип 1% [2]: в каждом интернет-коммьюнити есть 90% потребителей контента, 9% контрибьюторов и всего 1% тех, кто сам создает контент. Это не научное знание, но я в него верю: в моей практике оно часто подтверждалось.
Риск того, что девушки будут сидеть и ждать звонка, а эфир станет тягомотным ожиданием, был слишком велик. Так мы переработали концепт и решили, что каждая девушка, за отведенный ей час, должна рассказать короткую историю своей жизни. Структуру рассказа определяли заранее заготовленные вопросы, которые в общих чертах рисуют ее образ, как человека: как она относится к карьере, любви, смерти, сексу, дружбе, какие решения принимает, чего боится, за что готова бороться.
Мы хотели добиться максимальной вовлеченности от зрителя и истории должны были быть интересными — так, чтобы включил и залип надолго. Это значило подойти к задаче как фильм-мейкеры: учесть правила построения сюжета, выбрать актрис с навыками рассказчиков, создать законченные истории с завязкой, серединой и концом. В историях не должно было быть провисаний: в мире, полном контента, любую скучную историю закрывают быстро.
Для начала надо было правильно выбрать рассказчиков, которых подключат к полиграфу.
Мы искали открытых и эмоциональных девушек, таких экспрессивных экстравертов, которые готовы и посмеяться, и расплакаться, и не боятся камеры. Последний пункт был особенно важен — чтобы девушки не смущались показывать свои эмоции и лишний стресс не влиял на результаты полиграфа. Сделали кастинг среди студенток актерских ВУЗов из 120 претенденток выбрали всего 6. Мы искали разные типажи экспрессивных девушек — милых, и напористых, немного стеснительных, в общем, чтобы истории в кадре были быть разными.
6 девушек, потому что игра должна была продлиться 7 часов, а любой человек за 60 минут на детекторе лжи успокаивается, привыкает и его показатели смазываются.
Дальше надо было каким-то образом срежиссировать истории, которые девушки будут рассказывать. За 60 минут каждая девушка должна была стать «родной» зрителю, как персонажи сериалов, которым симпатизируешь.
Мы не могли заранее обсудить с девушками истории из их жизни и узнать, где лежит клад. Как мы уже рассказывали в статье про работу полиграфа [1], он показывает не правду или ложь, а всего лишь реакцию на стресс. Если вопросы не являются неожиданностью, показатели девушки во время стрима будут просто ровной линией и игры не получится.
Второй нюанс был в том, что вопросы на детекторе лжи подразумевают ответы «да» или «нет», без рассуждений. Это легко решалось четкой инструкцией актрисам: сначала однозначно ответить на вопрос, затем рассказать краткую историю, связанную с этой темой.
Так как же структурировать историю, которой ты не знаешь?
Нам был доступен только один инструмент: вопросы. Надо было каким-то образом придумать и в правильном порядке расположить 70 вопросов, каждый из которых начинал какой-то маленький эпизод общего рассказа.
Мы не стали изобретать велосипед и обратились к проверенным механикам из учебников для сценаристов, в частности, к классическому труду Memo. Секреты создания структуры и персонажей в сценарии Маккенна и Воглера. Горячо советую эту книгу, если вам интересна работа сценариста и вы любите рассказывать истории (даже друзьям за ужином), это настоящий альманах сторителлера.

Если кратко, то чтобы зритель «вошел в шкуру» персонажа и начал ему сопереживать (то есть не выключил кино), надо раскрывать героя в 4 этапа.
Так мы и поступили. Написали порядка 200 вопросов, сразу вычистили скучные и оставили 100 «горячих». Затем разделили их на категории и сделали предположение, какой, скорее всего, будет на него история: например, если спросить про самый стыдный эпизод жизни – это один эмоциональный окрас и уровень напряжения. Если спросить про страхи — другой. Если про первую любовь — третий.
В начале были вопросы, ответы на которые дают базовые характеристики, потом хардкорные (те самые, которые не принято задавать в приличном обществе — про деньги, страхи, секс и тд) и завершили милыми историями.
Мы не смогли все сделать идеально, но результат был хорошим: эта была самая вовлекающая игра за всю нашу историю спецпроектов. Показатель удержания был высоким: люди подключались и оставались на несколько часов, сам ntsaplin [3] пропустил обед в этот день :)
Давайте перейдем к технической части, там тоже было много интересного.
Всего в игре было 5 частей:
Трансляция велась отдельно через Open Broadcaster Software (OBS) и YouTube.
Вся наша система состояла из сервера и кучи клиентов. Одна группа была многочисленной — много клиентов девопсов, один клиент — веб страничка модератора, веб-страничка полиграфолога и много веб-страничек игроков.
Был еще один отдельный клиент системы — программа написанная на Котлине и работающая на компьютере полиграфолога. Всей игрой управлял именно он, обычный стационарный компьютер на Windows. ОС была принципиальной — программа Sheriff 7, в которой работал наш полиграфолог Миша есть только под Windows.

Игра состояла из циклов: свежий вопрос, ответ девушки, вердикт полиграфолога, вердикт игроков. Циклы контролировались событиями, которые отмечал в своем интерфейсе полиграфолог: начал задавать вопрос и получил ответ.
Для доставки этих событий мы использовали протокол mqtt. В нем есть брокеры (обычно один, подобие сервера), и есть клиенты. Клиенты умеют отправлять “сообщения” и подписываться на них, а брокер (сервер mqtt) просто экранирует сообщения всем клиентам, которые пожелали на них подписаться. Сообщения могут отправляться в разные топики, и клиенты могут подписываться не на все. Из контекста будет понятно, какие сообщения для кого в нашей системе, это главное.
Обычно клиентами выступают библиотеки с открытым исходным кодом, а брокерами — готовые программы или SaaS. В этой статье я буду называть клиентами в более широком смысле все сущности игры кроме сервера. Клиенты mqtt содержали все сущности, некоторые и не по одной.
Брокером в нашей системе выступил Mosquitto. Возможно многим читателям знакомы более популярные в энтерпрайз сфере другие MQ-протоколы — AMQP, RabbitMQ, ActiveMQ.
MQTT более легковесен, по умолчанию используется с одним брокером, имеет хорошую поддержку в js через websocket, есть удобные утилиты для ручной отправки и мониторинга сообщений из командной строки.
Всего у нас были две глобальные задачи: научиться расшифровывать графики в приложении Sheriff, чтобы отрисовывать их у игроков и решить проблему синхронизации времени на видео и в игре. Начнем с самого сложного — времени.
Одной из ключевых и самых важных вещей в игре было время. Нам было критически важно, чтобы активация кнопок в интерфейсе игрока и момент на трансляции конкретного зрителя строго (насколько это возможно) совпадали друг с другом.
Например, 15-секундный таймер у игрока должен был запуститься ровно в тот момент, когда девушка на его трансляции закончила отвечать на вопрос. Кнопки «правда» и «ложь» должны были активироваться только на время таймера и деактивироваться, когда отвечать было уже нельзя.
Для синхронизации видео и игрового процесса нужна была общая шкала времени. За ее нулевую координату мы взяли время начала видеотрансляции.
Все “игровые события” содержали в себе указание времени на этой шкале. Давайте называть его относительным временем (оно относительно начала трансляции). Каждое игровое событие содержит в себе относительное время, когда интерфейсу нужно его “воспроизвести”. Чтобы засекать время, даже относительное, нужны часы, то есть функция времени.
Самой важной и является последняя функция — она помогает синхронизировать видео и кнопки интерфейса у зрителя.
Как она работает:
Видеозапись от стартового пистолета до фотофиниша, на которой Усейн Болт ставит мировой рекорд на 100 м длится точно 9.58 секунд. Ожидается, что и функция getDuration() для прямой трансляции будет возвращать приемлемо точное время с начала трансляции. Она монотонна, непрерывна и даже линейна. Последнее математически не требуется, но без учета всяких СТО Эйнштейна является и свойством обычного времени тоже.
Как мы использовали эту функцию у игрока?
На страничке игрока есть iframe c плеером YouTube внутри — там отвечает на вопросы девушка. У плеера есть API, один из методов которого — упомянутый ранее getDuration(). Полученное событие нужно воспроизвести через
event.relativeTime - iframeAPI.getDuration()
Федор, наш разработчик, сделал простенький шедулер, который этим и занимался — получал игровые события и откладывал их обработку через setTimeout.

Вердикт полиграфолога (правду сказала девушка или ложь) — одно из множества сообщений, предназначенных для страницы игрока. Это не событие, а просто сообщение, в котором записан идентификатор вопроса и ответ — правда или ложь. Страница игрока сохраняла ответ и использовала его по идентификатору. Когда у игрока истекало 15 секунд, фронтенд сообщал ему, верно ли он угадал.
Тут очень приятно (нет) было пользоваться типом boolean. В игре есть ответ девушки — true/false, есть ответ игрока true/false, есть ответ полиграфолога true/false и угадал игрок ответ или нет — true/false. Не так уж сложно писать условия, но вот устно объяснять этот момент было непросто.
Следующий цикл игры начинается не по истечении 15 секунд, а только когда девушка перестанет рассказывать историю и полиграфолог отметит начало нового цикла событием pgSpeaks.
Логике игрока нужно было вовремя использовать сообщения об игре, полученные через mqtt: например, отрисовывать графики, которые приходили ему в виде комплектов по 6 чисел и сгруппированные в чанки длиной в физическую секунду.
Нашей задачей было перехватывать данные, которые выдает полиграф и рисовать их самостоятельно на сайте красивыми графиками.

Полиграф КРИС, с точки зрения компьютера — абстрактное COM-USB устройство. Что в этом было хорошего, так это то, что для работы он подключается компьютеру.
А вот то, что работать с ним умеет только допотопная программа Шериф 7, документация к которой только пользовательская и официально поставляется она вместе с бинарником в вот таком виде:

Как же получить данные о дыхании, сердцебиении и прочем?
Федор думал о двух вариантах:
Первое кажется самым фундаментально верным, второе сэкономило бы время на получении чисел и их можно было бы сразу передавать.

И оба решения нам не подошли, потому что:
Тем более, работает Шериф на Windows, а Федор привык к Linux или, на крайний случай, MacOS.
Так Федор решил пойти в лоб: записать скринкаст монитора полиграфолога на обычном допросе и использовать его, чтобы наладить чтение графика с экрана. На одном мониторе он запускал зацикленный скринкаст, а на другом писал код и иногда его запускал, направленным на второй монитор, и сразу наслаждался тем, как именно он не работает.
Федор находился в Питере (а вся остальная команда проекта в Москве, включая полиграфолога Мишу), так что он нашел полиграфолога на Авито, прихватил с собой жертву, посадил ее на ровный такой же полиграф и записал скринкаст допроса. В сыром виде без сжатия и цветовых манипуляций, последовательность bmp-файлов получилось только в 30 fps. Если кому интересно, 2.5 минуты 1920х156 заняли 1Гб, который очень приятно сжимался до 30 Мб с помощью 7z.

Он достаточно примитивен:
Делаем скриншот, проходим getPixel-ем один столбец в правом краю полотна и получаем 6 чисел — 6 точек на графике. Проходим другой столбец — по меткам в правом краю полотна и получаем вторые 6 чисел — центры шкал каждой из 6 величин.
В шерифе их можно двигать, увеличивать и уменьшать так, чтобы в зависимости от амплитуды, все показания попали в полотно. Попарные разности и есть наши 6 величин в неких условных единицах, которые нам совершенно неважны — важно только их изменение. Их подготавливаем к отправке вместе со временем скриншота.
Федор выбрал Kotlin/JVM и занимался разработкой распознавателя графиков под линуксом. Метод java.aw.Robot.createScreenshot показал впечатляющую производительность: больше 1000 fps на частичных скриншотах (ему не весь экран был нужен, а только краешек), пришлось еще отсеять повторяющиеся скрины.
В качестве отладочного вывода Федор рисовал в файлик 1920х1080 участок кривых соответствующей длины так, как они прочитались. Получилось так

Учитывая, что Федор работал со скринкастом в 30 fps, на живом Шерифе точно должно получиться хорошо, но… Не получилось.
График во фронтенде оказался нечитаемо ломаным, даже хуже чем в отладочной картинке.
Выяснилось, что Windows заботливо ограничивает частоту работы native методов позади Robot.createScreenshot 30-ю снимками в секунду. Стало ясно, почему скринкаст не получилось сделать больше 30…
Возможно, он заметил бы это раньше, если бы вышел из зоны комфорта, пересев на время написания читалки графиков с линукса на Windows, но вышло, как вышло. Всё равно у него не было материала больше 30 fps (на этом моменте рассказа, Федор промакивает глаза салфеткой, прим. интервьюера)
Но решение нашлось: расширить зону захвата и с одного скрина снимать 6 точек не по одной координате, а сразу по нескольким крайним. Точнее по ровно стольким, сколько новых появилось с последнего скриншота. Тут нужно было как-то ориентироваться, насколько полотно в Шерифе сдвинулось с момента прошлого кадра — помогли вертикальные белые линии сетки.
Федор сделал область захвата такой, чтобы попадала хотя бы одна, по ее перемещению и вычислялось, сколько пиксельных столбцов нужно пройти после очередного кадра. Проверить до прогона с полиграфологом эту модификацию он не смог — даже под виндой у него все еще был только 30 fps скринкаст. Однако отладочный вывод на нём выглядел идеально:

Но на прогоне графики с первого раза заработали идеально и непосредственно с ними проблем больше не было.
Веб-странички игроков получают графики так же через mqtt, событиями. В каждом событии относительное время и 6 чисел (дыхание, сердцебиение, потоотделение и так далее) — величины записанные в этот момент.
События с величинами складывались в чанки по несколько штук в mqtt-сообщение — столько, сколько снялось за секунду, около 50 штук. Они складывались в очередь на отрисовку в соответствующие TimeChart-ы. Библиотека отрисовки сама двигает плоскость графика и ставит точки на нужном расстоянии друг от друга по оси времени (гарантий строгой периодичности, к сожалению, не было).
Одна из технических сложностей — рисовать 6 графиков величин от реального времени (едущих слева-направо), в масштабе ~20 секунд и с высокой плотностью точек.
Удобный и красивый ApexCharts.js и близко не подошел по производительности — fps порядка единиц и огромная загрузка ЦП. Альтернативы тоже не радовали: по запросу “high performance realtime charts” ничего интересного не находилось.
Спас чат Саматика @ctodaily, куда Федор написал и посоветовался насчет библиотек. Так стало понятно, что почти все либы рисуют графики svg, стало быть, нам надо было искать “webGL charts”.
Из быстро нашедшихся webgl-plot, lightningcharts и TimeChart, Федор выбрал последний. С производительностью же у всех было хорошо, в результате получилось обновлять графики с частотой около 50 fps, как раз с такой частотой полиграф и отдавал значения.

Помните идею синхронизации через функцию getDuration ютьюба? Звучит просто, если бы не небольшая тонкость.
Чтобы видео и игровой интерфейс игрока были синхронными, было недостаточно просто взять и нажать на кнопку начала трансляции на компьютере полиграфолога в непосредственный момент ее начала.
Дело вовсе не в миллисекундах человеческой реакции между нажатием кнопки «начать трансляцию» и нажатием кнопки Start streaming в OBS. Если бы дело было лишь в этом, мы могли бы воспользоваться API OBS.
Перед отображением у игрока первый кадр видео проходит долгий путь:
Все это, в особенности доставка, занимает неопределенное время, обычно от 2 до 8 секунд. Эту величину мы решили однократно подбирать для каждой трансляции и добавлять вручную перед началом игры. Чтобы высчитать время, поставили монитор полиграфолога так, чтобы видеть его на трансляции.

В интерфейсе игрока создали отдельный “режим синхронизации”. Федор подключался к нему и делал следующее:
По идее, страница игрока-синхронизатора пыхнет не сразу, а в то же относительное время, что пыхал монитор полиграфолога, и с учетом коррекции. Если коррекция ноль, то временной интервал между пыхом ПГ и пыхом фона синхронизатора и есть коррекция.
Как мы проводили первоначальную синхронизацию перед игрой:
Принципиально важный момент: нужно было мерить не лаг трансляции с youtube между отправкой команды и пыхом, а время между пыхом полиграфолога и ответным пыхом на него в относительно времени. То есть мы измеряли величину отставания относительного getDuration-времени от относительного времени, которое нам хотелось бы иметь — с нулем точно в начале трансляции, а не на 2-8 секунд раньше.
Таким образом, один цикл ручной синхронизации занимал 1-12 секунд лага youtube для игрока, плюс 2-8 секунд лага между OBS и сервером youtube.
Когда Фёдор тестировал решение в окружении разработки, оно сработало идеально. На первом прогоне на съемочной площадке тоже.
Но через 20 минут на съемочной площадке просело подключение к Интернету — до 1.5 МБ/с, пинга 100 и какого-то сумасшедшего трёхзначного джиттера и тогда синхронизация сбилась.
Даже у игрока на трансляции появились артефакты. Выяснилось, что какие бы ты настройки в OBS не выставлял, при проседании интернета происходит буферизация кадров, которые не успели отправиться, а на сервере youtube в свою очередь перерыв в видеостриме.
На время этого перерыва, видеофайл, передаваемый пользователю, не прерывался и getDuration-время продолжало расти. При таких условиях зритель будет видеть установку 9,58-секундного рекорда в течение 12 секунд. Не слишком логично, что getDuration считает время наблюдателя, а не время события согласно документации.
Дальнейшие эксперименты показали, что линейность времени события нарушается скачками, да еще и с завидной регулярностью.
Каждый раз передвигать монитор полиграфолога в кадр, чтобы синхронизироваться по-старинке, вручную было нельзя — портился тщательно выставленный кадр. Времени до запуска проекта оставалось мало и Федя просто добавил полиграфологу второй монитор — маячок, который бы отвечал только за синхронизацию и всегда находился в кадре.
Также, он написал автоматический синхронизатор, который автоматически выполнял синхронизацию, которую Федя делал обычно вручную.
Синхронизатор слал команду синхронизации, ждал пыха в трансляции, снимая скриншоты со страницы игрока-синхронизатора с линуксовой скоростью больше 1000 fps и засекал время до ответного пыха. Пыхи на мониторе-маячке мы сделали разных оттенков серого, чтобы зритель их не замечал на трансляции.
Чтобы протестировать синхронизатор, мы сделали стресс-тест: запустили трансляцию через 3G, раздаваемый c телефона на компьютер с OBS.
За 6 часов непрерывной трансляции удалось поймать два скачка времени, которые хорошо обработались. Через какое-то время вылез еще один рассинхрон и из логов стало ясно, что время не просто нелинейно, оно НЕ МОНОТОННО.
Оказалось, Youtube делает скачки в своей записи не только вперед, но и назад. Иногда на время, на которое был скачок вперед, иногда меньшее. Почему, а главное зачем он это делает осталось для нас загадкой. По всей видимости, функцию getDuration по назначению никто до нас не использовал…
Но по крайней мере, учесть скачки в другую сторону на этом этапе было уже просто.
Мы запустили проект 1 декабря и игра шла 7 часов. Всего в нее поиграли почти 9000 человек. Охват был не такой широкий, как в проекте с уничтожением сервера [5], но это с лихвой возместил коэффициент удержания. Вот запись игры
До новых спецпроектов в Новом Году! И спасибо, что дочитали лонгрид.
Автор: galimova_ruvds
Источник [8]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/news/360222
Ссылки в тексте:
[1] одну онлайн-игру: https://habr.com/ru/company/ruvds/blog/526166/
[2] принцип 1%: https://en.wikipedia.org/wiki/1%25_rule_(Internet_culture)
[3] ntsaplin: https://habr.com/ru/users/ntsaplin/
[4] сайт: http://liedetector.ruvds.com
[5] уничтожением сервера: https://habr.com/ru/company/ruvds/blog/515522/
[6] Image: https://ruvds.com/ru-rub/news/read/126?utm_source=habr&utm_medium=article&utm_campaign=galimova_ruvds&utm_content=bioxaker_entuziast_vzhivlyaet_kartu_trojku_pryamo_v_ruku
[7] Image: http://ruvds.com/ru-rub?utm_source=habr&utm_medium=article&utm_campaign=galimova_ruvds&utm_content=bioxaker_entuziast_vzhivlyaet_kartu_trojku_pryamo_v_ruku#order
[8] Источник: https://habr.com/ru/post/535398/?utm_source=habrahabr&utm_medium=rss&utm_campaign=535398
Нажмите здесь для печати.