Запилили стриминг на Flutter (вот он — проект The Hole). В процессе встретили немало проблем: недостаточная функциональность пакета видеоплеера, сложности с реализацией фичи картинка-в-картинке, специфические для платформ ошибки. Да, какие-то проблемы были из-за нашей неопытности и молодости технологии — мы стартовали проект в 2020 году. Были специфичные для Flutter баги. И были засады, которые неспецифичные для Flutter, но специфичны для натива — поэтому нативные разработчики, не спешите скроллить ленту дальше. Погнали! Пожалуй, самое время представиться. Меня зовут Тимур Тхаркахов. Я — Flutter-разработчик, и сегодня мы вместе с вами будем разбираться с теми самыми палками, которые Дэш так усердно вставляет в колеса тем, кто хочет разработать стриминг.  Немного предыстории. Я устроился в Surf в качестве разработчика и после онбординга узнал, что буду работать на проекте со стримингом видео. Скажу честно, был настроен скептически. Мой опыт Android-разработчика подсказывал, что работа с видео и на нативе непростая и полна подводных камней.  Что уж говорить о Flutter. Особенно учитывая желание заказчика иметь фичи воспроизведения в фоне и картинки-в-картинке. Первый же баг, доставшийся мне по наследству от коллег, отчасти подтвердил мои опасения: приложение вылетало в чёрный экран при первом запуске сразу после установки. Однако дальнейшая работа на проекте сильно пошатнула мою позицию в сторону Flutter: всё оказалось не так страшно. Если вы хорошо знакомы с Flutter, смело листайте до следующего блока Flutter — в первую очередь UI-фреймворк. Он концентрируется на отрисовке UI-компонентов, и это отчасти обуславливает особенности реализации механизмов связи с внешним миром. В нашем случае «внешний мир» — это Android и iOS, но Flutter поддерживает ещё Web и Desktop. Для работы Flutter использует виртуальную машину Dart. Код, исполняемый в рамках виртуальной машины, работает в изолированном окружении, поэтому на разных платформах можно использовать не только общий UI, но и бизнес-логику приложения.  Однако по той же причине многие «нативные» возможности напрямую из Dart недоступны. Команда Flutter реализовала механизм взаимодействия с нативом при помощи каналов. Мы можем после небольшого изучения документации настроить связь с платформой и вызывать как нативный код из Flutter, так и Dart-код из натива. Остановимся подробнее на одной фиче, которая особенно важна для нашего повествования. Я говорю о возможности прокинуть нативный UI-компонент (View, UIView) во Flutter, встроить его в дерево виджетов и работать с ним из кода на Dart. Это позволяет использовать существующие наработки сообщества, чтобы не писать с нуля сложные компоненты. Например, именно таким образом работает плагин google_maps. И так же работает один из героев статьи — video_player, под капотом которого — известные нативные компоненты AVPlayer для iOS и ExoPlayer для Android. Именно этот плагин выбрала команда Surf в начале разработки проекта. Надо сказать, что до сих пор, больше двух лет спустя, серьёзных и качественных альтернатив ему почти нет. Потенциально интересным мне кажется только flutter_vlc.  Есть только базовая функциональность: например, отсутствует возможность сбора логов о состоянии воспроизведения. Разное поведение плеера при восстановлении соединения с интернетом на Android и iOS. Например, на Android единственный способ возобновить воспроизведение видео после возвращения связи — пересоздать экземпляр плеера. На iOS просто нет ошибки, сигнализирующей об отсутствии интернета: приходится полагаться на данные стороннего пакета. Неконсистентность флагов buffering и loading при нестабильной связи, а также отсутствие подробных сообщений об ошибках. Рост количества багов в крашлитике после взрывного роста пользовательской базы (никогда такого не было — и вот опять). Причины большинства проблем крылись именно в плагине видео плеера. Первым делом мы завели issue в репозитории пакета. Но очевидно, что далеко не все запросы новой функциональности и не все баги — приоритет для команды поддержки.  Поэтому мы с командой сделали форк плагина, провели исследование и составили список возможностей, которые было важно доработать. Среди них: Расширенные сообщения об ошибках. Возможность стартовать воспроизведение сразу с нужной позиции.  Возможность устанавливать размер буфера и обратного буфера.  Сбор подробной статистики воспроизведения: ширина канала, время буферизации, потерянные кадры.  Потенциальная возможность работать с плейлистами — и другие возможности. Не все из них мы реализовали. А те, что реализовали, далеко не все получились идеально :slightly_smiling_face: Однако это позволило решить большую часть проблем пользователей. Оглядываясь назад, мы не жалеем, что использовали именно пакет video_player: он позволил быстро завести проект и проверить гипотезу. Однако времени и сил на доработки мы потратили много. Поэтому вариант сразу писать свой плагин с нуля выглядит не самой глупой затеей — особенно если в команде достаточно опыта работы с целевыми платформами. Или easy to learn, hard to master Функциональность воспроизведения в фоне оказалась несложной для внедрения. Мы использовали библиотеку audio_service. Но позже, в связке с другими фичами, она попортила нам немало нервов.  Например, один из памятных багов — «чёрный экран» при первом запуске приложения после установки из стора. Появлялся на некоторых устройствах Samsung и OnePlus. В upstream он так и не попал — даже после общения с мейнтейнером и появления похожего поведения у других разработчиков. Проблема оказалась в некорректном подключении приложения к FlutterEngine во время запуска Activity, если тот был ранее запущен в Service — именно там обрабатываются события вроде нажатия кнопок гарнитуры. Локализовать эту проблему помог нативный опыт и знание жизненного цикла компонентов Android. Так мы получили в pubspec ещё одну красивую ссылку на наш форк в гитхабе. Позже дорабатывали функциональность библиотеки для корректного взаимодействия с гарнитурами, а также приглушения и восстановления звука при входящем звонке. Большая часть кода для правок, о которых мы сказали выше, написана на нативе:  Android — Java, Kotlin;  iOS — Swift и, прости господи, Objective-C.  Да-да, Objective-C! Можем немного соврать (не сильно), но, кажется, iOS-часть всех пакетов, поддерживаемых командами Google в репозитории flutter/packages, написана на Objective-C. Найти iOS-разработчика, готового (и горящего желанием) помочь поправить или написать новый код на Objective-C, может оказаться тем ещё квестом. Зато для Flutter разработчика в Surf — это часть T-Shape развития :wink: Именно в эту область погрузился один членов нашей команды (при участии iOS-разработчика, конечно: мы поощряем менторинг). Ему предстояло написать iOS-часть плагина, предназначенного для поддержки функции «картинка-в-картинке». Несмотря на то, что код писали на Swift, пришлось хорошенько покопаться во внутренностях пакета video_player, написанного на Objective-C, чтобы «подружить» их. Нам нужно было: Получить из плагина плеера textureId, которую использует плеер для отрисовки кадров видео.  Передать её в наш плагин.  По textureId получить ссылку на плеер.  В процессе не забыть о жизненном цикле iOS-приложения, Flutter-виджетов и воспроизведении в фоне.  Та ещё задачка! Кстати, с выходом Flutter 3.7 разработчики анонсировали миграцию этих пакетов на Swift.  Для приложения, основная функция которого — воспроизведение каталога видео, очевидно, нужен и вертикальный каталог видео с предпросмотром.  Хотите такой во Flutter — не проблема: вам не придется возиться с RecyclerView на Android или UICollectionView и его друзьями на iOS (ммм, вкуснятина). Во Flutter есть замечательный виджет ListView, который умеет лениво отображать условно-бесконечный список. Хотите, чтобы в каждом элементе списка был контроллер видео и при попадании в фокус начиналось воспроизведение — легко. В первой реализации наши превью в ленте начинали воспроизводиться мгновенно после попадания в фокус! Однако не забывайте, что концепция Flutter «всё — виджеты» хоть и упрощает разработку, компоновку и переиспользование элементов UI, но требует внимания в пограничных случаях. Виджеты во Flutter имеют свой жизненный цикл. И вследствие концепции «всё — виджеты» это распространяется и на экраны приложения. Здесь нет аналогов Fragment, Activity или ViewController. А значит, возможно и специфическое поведение на стыке виджета и нативного компонентов. Как вы думаете, сколько одновременно «живых» экземпляров видеоплеера мы можем позволить себе иметь на ленте в примере с ListView? Ответ не такой очевидный, как может показаться. В результате поиска ответа на этот вопрос, изучения Stackoverflow, исходников пакетов и даже (sic!) документации, поскребя по сусекам интернета мы взялись за э-э-э-эксперименты.  И самые светлые QA-головы в Surf пришли к следующей табличке: И сейчас мы отвечаем на вопрос так: надо стремиться поддерживать один и только один активный экземпляр плеера в приложении.  Ответ можно назвать универсальным: он касается не только Flutter, но и нативных приложений. Вы обращали внимание, что приложению ютуба нужно заметное время, чтобы при скролле ленты начать воспроизведение следующего превью? Ну вот и мы скосплеили ютуб немного пожертвовали пользовательским опытом в пользу стабильности. Если на ленте есть два активных экземпляра плеера, то открытый экран эпизода и третий экземпляр плеера могут окончательно высосать все доступные ресурсы: видео просто не будет работать. Ресурсами устройства надо управлять разумно и освобождать их, когда они не нужны. Не все пользователи владеют iPhone 20 про макс экстра гейминг эдишон: некоторым устройствам может не хватать памяти и производительности, чтобы работать с большим количеством ресурсов одновременно.  Кроме того, мы — не единственное приложение на устройстве, и пользователи будут благодарны за оптимизацию. Так что не забывайте оставлять отзывы, когда всё работает, а не только когда не работает :wink: Нужно иметь в виду, что на разных устройствах используется разные аппаратные декодеры. Если видео закодировано в определённом формате, оно может воспроизвестись на 8 из 10 устройств, но не воспроизведётся или будет сильно тормозить на оставшихся двух. Более того, не все драйверы честно сообщают системе о своих возможностях. Например, в ExoPlayer встроена функциональность автоматического выбора качества в зависимости от множества факторов: ширины интернет-канала, разрешения экрана, аппаратных возможностей устройства.  Мы встречали немало бюджетных Android-устройств, которые заявляют плееру о том, что готовы воспроизвести 1080p видео и это была ложь, а в реальности не справлялись с этим. Именно это привело нас к решению добавить в плагин плеера функцию сбора информации о потерянных кадрах. В iOS свои особенности. Вы не можете указать AVPlayer размер буфера для видео — только желаемое значение. Которое будет использовано. Наверное. По четвергам. В чётные даты. Ещё один пример. В HLS-плейлисте указаны доступные параметры качества видео. Мы хотим дать пользователю возможность выбрать конкретное качество, даже если автоматически доступно более высокое. Это может быть полезно для экономии трафика, например, в роуминге. AVPlayer, конечно, содержит метод для установки предпочитаемого качества, но де-факто работает так, как посчитают оптимальным его внутренние алгоритмы. Не забывайте об особенностях платформ. Некоторые функции могут быть доступны только на одной из них. Если метод, корректно работающий на одной платформе, странно ведёт себя на другой или совсем не работает, посмотрите документацию или реализацию в коде. К счастью, исходники подавляющей части пакетов открыты. 1. Ютуб не просто так берёт с вас деньги за фичу воспроизведения в фоне. 2. Если вы пришли во Flutter, соблазнившись удобством написания UI и анимаций (как и я :slightly_smiling_face: ), не забывайте «базу»: знание нативной разработки всегда полезно. 3. Если путь в мобильной разработке вы начали именно с Flutter: изучайте особенности платформ — это будет вашим конкурентным преимуществом. Мы можем вспомнить ещё кучу сложной, забагованной и просто прикольной функциональности, часть которой не дошла до релиза:  защита от скриншотов и записи экрана,  вотермарка над видео при записи,  некрасивая анимация при повороте экрана на iOS,  борьба с FFmpeg при нарезке клипов,  реакции при просмотре видео,  куууча классных UI компонентов и анимаций,  поддержка AirPlay и Chromecast,  обеспечение доступа к видео из разных частей мира (даже оттуда, где доступа быть не должно),  прероллы к роликам (реклама — двигатель торговли :wink: ),  переезд на Flutter 2 и null safety (больно),  обновление до Flutter 3 (не больно),  переход на Elementary (совсем не больно),  куча оптимизаций на бэке, редизайн, тесты, тесты и ещё раз тесты (и всё равно баги :mending_heart:). Но об этом как-нибудь в следующий раз.  А пока приглашаем вас подписаться на телеграм-канал Surf Flutter Team. Там мы рассказываем о кейсах, подобных этому, публикуем вакансии и обзоры фишек Flutter, проводим эфиры и вообще наводим движ.  Перейти в канал Surf Flutter Team >>