Comments 58
Так уж получилось, что похоже я практически в то же время натолкнулся на (эту же?) проблему 40ms
но в контесте WebRTC
в приложении под Android
после сворачивания: в одном случае решилось заменой использования pthread_cond_timedwait_relative_np
на pthread_cond_timedwait
, в другом случае починилось заменой nanosleep(0)
на sched_yield
. Правда до кода в Android, вызывающиего проблему я так тогда и не докопался.
Чорт. У нас сейчас тоже очень похожая проблема c ExoPlayer. Видео играет несколько секунд, stall на 0.5 сек, играет, stall,… а в логах — audio buffer underrun.
Гениально. Такие глубокие познания.....
С ума сойти, приложение 200-миллиардной компании работает только потому, что в многопоточный мобильной операционной системе кто-то гарантированно его дёргает ровно раз в 16,6 мс. А если не дёргает, то не работает.
… меня например больше всего поразил вот этот фрагмент:
Почему бы при каждом вызове обработчика не копировать больше данных? Это была справедливая критика, но реализация такого поведения повлекла бы глубокие изменения, на которые я не хотел идти, поэтому продолжил поиски первопричины. Я нырнул в исходный код Android
Что это за такие глубокие изменения, что вместо «увеличить буфер под данные в 2 раза» (например) быстрее вышло проанализировать исходники Андроида?
… разве что задачей на самом деле стояло написать отчет «почему это не моя вина, и почему я ничего у себя менять не буду»
Потому что меняя что-то тут, нужно будет протестировать все остальные девайсы и все остальные оси на предмет "оно всё ещё работает как надо", а если нет — это очень дорогие изменения.
Либо ставить костыль "если этот вендор на этой версии андроида, тогда".
Код написано исходя из текущих ожиданий, но никто не гарантирует что завтра Google не придет и не скажет: «теперь мы делаем вызов каждые 20 мс» и все равно придется переписывать приложение под новые версии, или «теперь мы делаем вызов каждую 1 мс» и это вызовет другие проблемы.
Разумеется, гарантий нет, но риски никто не отменял. Лучше потратить день и докопаться, чем быстренько починить и потом разгребать. И гугл скажет про то, что будет в следующей версии (для этого есть change notes между версиями), а я говорил про риск поломать сейчас в другом месте.
Можно размер буфера сделать динамическим, в случае если аудиобуфер опустошается быстрее заполнения — динамически подстраивать размер выгрузки.
IMO Это будет чуть более future-proof решение чем полагаться на магические константы.
(upd: сорри, промахнулся уровнем комментария)
качественное видео на самом деле жрет ресурсы не хуже многих динамичных игр
Не жрёт. Пруф очень простой — на iPad можно часами напролёт смотреть видео в высоком разрешении с Youtube от батарейки.
На пользовательских устройствах его декодирует аппаратный декодер и пишет кадры напрямую в память видеокарты. Это крошечный ASIC с потреблением в доли Вт, который, как правило, лицензируют у ARM вместе с GPU.
Причём афтар статьи сам об этом пишет, но архитектура кода не позволяет.
А ведь это ключевое приложение многомиллиардной корпорации, с зарплатами разрабов по 200+.
С ума сойти, все приложения компаний с капитализацией > 10 триллионов работают только потому, что вокруг триллионы кусочков кремния правильно делают +1 после выполнения инструкции INC на десятках разных архитектур и десятках тысяч разных моделей этих кусочков кремния. И делают это уже последние лет 50.
Что-то как-то мимо. Любая функция отложенного выполнения, будь то sleep, set_interval или set_timeout, никогда не гарантирует, что выполнение будет отложено ровно на указанное количество милли/наносекунд. Это всегда остается на совести планировщика/ос и сильно зависит от текущей нагрузки. Полагаться на определённое поведение очень самонадеянно, в чем и убедились герои статьи.
Ага, не гарантирует. И это "не гарантирует" кое-как компенсировать научились. Но нормально компенсировать можно только однократную небольшую проблему, а не крупную повторяющуюся.
Иными словами, есть некоторая разница между запозданием пробуждения потока на 1 миллисекунду (что нормально) и постоянным лагом в 40мс.
Вне систем реального времени никто не гарантирует, что любой системный вызов будет завершён в разумные сроки (я как сисадмин с этим постоянно сталкиваюсь в районе дискового IO, у которого могут случаться "чудеса" на scsi шине с участием умирающих дисков).
Таким образом, полагаться на то, что sleep закончится за 16 мс ровно так же наивно, как и полагаться, что ioctl для swap buffers в видеоадаптере будет выполнен за vblank время. А, вдруг, он 300мс займёт? А потом ещё раз. И ещё раз.
А нужно 60 fps. И что делать? Тут ситуация такая же.
На самом деле это очень распространненый паттерн работы плейеров :( он настолько древний, что сказать кто и когда его написал первый раз невозможно. Но он упорно копируется из плейера в плейер. Особенно подвержены этому плейеры в различных приставках, ТВ и мобильных устройствах.
При любой периодической задаче нужно делать не работу, запланированную на время запуска, а работу, которую нужную было сделать к реальному моменту срабатывания.
Например, если у вас анимация, нужно узнать текущий момент времени по сравнению с конечным и подвинуть фигуру на соответствующее место.
Если у вас обсчёт событий по крону раз в день, нужно выбрать не события за предыдущие 24 часа, а с момента предыдущего обсчета.
Если вам нужно подгружать кадры в кодек, нужно подгрузить все что были пропущены за прошедшее время, а не один.
Конкретно для озвученных таймингов (55мс ожидания и 16,6мс на кадр) подобная "подстройка" приведёт к пропуску трёх кадров из каждый четырёх. Ну да, формально видео будет показываться, но мне почему-то кажется, что 15фпс вместо 60 — не то что нужно пользователю.
Когда требуется показывать 60 кадров в секунду — лишняя задержка в 40мс является недопустимой и не поддаётся никакой компенсации.
Такое было бы возможно, если бы кодек от задержки данных не останавливался, а продолжал отмерять виртуальные секунды и показывал только те кадры, которые успели доставиться. Однако в реальности при задержке данных (например, проседание сети), кодек останавливается на том месте, где у него было недостаточно данных и продолжает воспроизведение когда эти данные до него доходят.
То есть максимум был бы один легкий подтормоз в начале воспроизведения, который даже меньше времени реакции человека.
Что это за такие глубокие изменения, что вместо «увеличить буфер под данные в 2 раза» (например) быстрее вышло проанализировать исходники Андроида?
Ну допустим что в приложении заполнение буфера делается некой библиотекой, общей для всех Android устройств. Каковы дальнейшие действия?
а) увеличить буфер в апстриме либы.
б) увеличить буфер после проверки некоего флага в апстриме либы
в) создать форк либы под это устройство
Первые два варианта мало того что являются костылем так еще требуют мегатонны согласований(Netflix то не маленькая) окончательно хороня надежды вписаться в дедлайны релиза, третий вариант сам по себе редкое дерьмище в плане последующей поддержки этой либы.
А что в итоге?
В сорцах Android есть баг, ок.
Из оригинальной статьи не понятно:
- Код Netflix нативный или на Java/Kotlin?
- Какое API используется для декодирования: штатный MediaCodec из Android SDK/NDK?
2.1 Если да, то почему бы не пихать данные в декодеры с максимальной скоростью? (синхронизация ведь все-равно произойдет на стороне рендеров) - Как была решена проблема?
3.1 Увеличением размера буферов
3.2 Использованием других примитивов синхронизации (а какие были до этого?)
3.3 Перепрошивкой всех приставок более новой версией ОС?
...
Это классическая ситуация, когда в прейбечном пэйплайне одна из веток (аудио/видео) подпирает другую.
Собсвенно для решения подобных проблем и делают асинхронные очереди по каждой ветке.
Забавно, что автору было дешевле изучить код AOSP, чем адаптировать свой код к суровой реальности.
По моему очевидно что они пофиксили создание тредов чтобы проблемный не становился бэкграунд тредом и шедулился нормально.
Непонятно они пересобирали ядро на 5й версии или пересобирали все приставки на 6й.
Чувак молодец, что нашел известный баг, но говорить «нетфликс не будет работать на 5й версии» (но другими словами) тоже неправильно.
Все, кто длительное время разрабатывает под андроид, встречали разные баги ядра, от которых никуда не деться, но никто не говорил что «ну, живите с этим».
Но это не фикс для плейера. Это просто воркэраунд позволяющий скрыть проблему.
Почему это?
Потому, что синхронизация плейбэчных мультимедиа графов должа происходить в конечной точке — аудио/видео рендере, а не где-то по пути перед декодером.
Так работают медиафреймворки DirectShow/GStreamer/OpenMAX и в них нет таких детских болячек, как в сабжевой статье.
Разумеется, если мы пишем аудио рендер, например на Qt, с использованием QAudioOutput, то должны повысить приоритет потоку, разгребающему входную очередь до QThread::TimeCriticalPriority. В противном случае получим заикания.
У нетфликс своеобразное решение, они подпирают аудио/видео ветки посередине.
При том, что потом будет еще один подпор на рендере (скорость работы DMA для звукового ЦАП никто под нас подстраивать не будет, оно потребляет данные монотонно со скоростью штатного кварцевого генератора).
Как была решена проблема?
В комментариях к оригинальной статье автор ответил, что Интегратор бэк-портировал багфикс андроида (вышедший для Marshmallow) под Lollipop (под которым работал телевизор). Я так понял, что приложение нефликса вообще менять не пришлось.
The integrator back-ported the patch (linked above) to L. It is literally only a few lines so this was straightforward. Some of the hardest problems take 1 or 2 lines to fix :)
Some of the hardest problems take 1 or 2 lines to fix
Как же это знакомо. Дни (и ночи) дебага с периодическими комментариями «да как так то бл...» заканчиваются фиксом в одну-две строчки.
Интегратор бэк-портировал багфикс андроида
Получается какая-то антиреклама.
Netflix выявил у себя архитектурную проблему и осознанно отказался фиксить.
А ещё можно починить такой баг, но у десятков/сотен приложений поломаются костыли которые на него опирались.
Классика: работает, не трогай.
Подобная "архитектурная проблема" есть у любого аудио- или видеопроигрывателя, а также в большинстве игр.
Всё-таки это ответственность ОС — следить чтобы те приложения, с которыми работает пользователь, получали больше процессорного времени.
Самый Главный Отдел: так, 60FPS, некая магическая константа «15мс», здесь гвоздями прибьём, таак падажжи.
Звучит смешно, но удели они достаточное внимание кешированию нескольких кадров и куче подобных не всплывших упрощений, они могли бы и не успеть выйти на рынок.
Похоже, что у нетфликса контент предзакодирован специально для их плеера(ов).
В результате декодеры работают с идеальными входными данными и плеерописателям можно "не напрягаться"
Ну это наверное самая большая проблема всех стриминговых сервисов :( приходится делать стримы под конкретные плейеры. И это реальная боль.
Так у них нет сторонних плееров. У них свое приложение под все распространённые программные платформы.
Я имел ввиду общую проблему всех стриминговых сервисов. Свой плейер решает некоторое количество проблем, но добавляет другие. Только просто протестировать плейер на разношерстной братии теликов, боксов, писишек и тп требует очень больших усилилий и ресурсов.
Конечно это не просто, но для такой большой компании не думаю что сложная задача.
Я подробностей не знаю, но думаю что у них есть сертификация клиентского оборудования и так далее (это видно из переведенной статьи). Просто так в рандомный телик ее не запихнут. Айфоны-айпады протестировать просто — их мало. Основная проблема, думаю, с разношерстными андроидами.
Сейчас работаю в телекоме, как раз в IPTV, и у нас есть в STBшке (внутре у ней андроед, но пользователям не доступен, только оболочка) приложуха Netflix. И есть тестовый аккаунт в Netflix для проверки и прочего. При случае узнаю у причастных как ее туда пропихивали...
public static void spinCpu(final long spinMs) {
final long startMs = System.currentTimeMillis();
int count = 0;
while (System.currentTimeMillis() - startMs < spinMs) {
count++;
}
}
Проблема решена, но какова цена :)
В Android всё точно так же: с версии 2.3 поддерживается OpenSL ES, где можно создавать очередь из буфферов, в более поздних версиях появились новые API. Просто не нужно говнокодить как в Netfilx. Я писал систему потокового вещания поверх RTP, и не было никаких проблем ни в Jelly Bean, ни в Marshmallow.
Запуск Netflix на телевизорах и приставках. Лишние 40 миллисекунд