Live Streaming on iOS

В нашей работе нередко возникает необходимость реализовать отправку видеопотока с iOS-устройства в реальном – или близком к реальному – времени. Самый частый пример — использование iOS-устройства в качестве камеры слежения или создание стриминговых приложений наподобие Periscope. Как правило, при возникновении подобной задачи ставятся дополнительные условия — например, возможность проигрывания потока на другом устройстве (или в другом приложении) без возникновения лишних проблем в браузере или VLC-плеере, малые задержки (видеопоток должен транслироваться практически в режиме реального времени), низкая нагрузка на устройство (возможность длительной работы от батареи), отсутствие необходимости в специализированном медиасервере для обслуживания передачи потока, и т.п.

Continue reading

Оптимизация отрисовки waveform на iOS

В статье пойдёт речь о том, как визуализировать вейвформу оптимальным образом, что для этого подходит лучше – CAShapeLayer или drawRect(), а также о некоторых тонкостях работы со Swift. Информация будет полезна не только тем, кто разрабатывает сложные кастомные UI компоненты и работает со звуком, но и любым iOS-разработчикам для расширения кругозора.

Постановка задачи

Разработчики DENIVIP создали приложение для фильтрации шумов в аудиозаписи – Denoise. Для пользователя это выглядит примерно так: он выбирает видео из своего альбома, приложение строит вейвформу звуковой дорожки и пользователь может выбрать на ней область с шумом, который нужно подавить. После небольшой магии пользователь получает видео с отфильтрованным звуком.

Алгоритм подавления шумов в данной статье подробно не обсуждается, но по большому счету он не выходит за рамки университетской программы по цифровой обработке сигналов. Теоретическую базу можно найти в удобном виде на сайте MITOpenCourseWare. Если говорить очень кратко, когда пользователь выбирает участок с шумом, мы, используя прямое преобразование Фурье, получаем частотный спектр «шумного участка». Запомнив эти частоты как «плохие» и требующие подавления, алгоритм проходит по всей аудиодорожке, на каждом участке получает частотный спектр и применяет подавляющие коэффициенты к «плохим» частотам. Затем, после применения обратного преобразования Фурье, получается новая вейвформа (дискретные звуки), в которой шумы существенно подавлены. Вся сложность состоит лишь в том, как качественно выбрать «шумный» участок. При этом чем длиннее видео, тем важнее становится интерактивность вейвформы, т.к. пользователю может потребоваться выделить несколько секунд аудиодорожки в середине двухчасового видео.

В первую очередь нам необходимо отобразить звуковую дорожку в понятной для пользователя форме и при этом дать ему возможность манипулировать ею (изменять масштаб, перемещаться по ней, выделять фрагмент). Сразу замечу, что если мы попытаемся отобразить все сэмплы нашей аудиодорожки, то может оказаться, что, во-первых, нам не хватит памяти, а, во-вторых, процессор (а заодно и GPU) “захлебнется” от такого количества точек.

Пример. Для 10-минутного видео 16-битная стерео аудиодорожка с частотой дискретизации 44,1 кГц будет «весить» 10 * 60 * 2 * 44100 * 2 ~ 100 MB. При этом большая нагрузка ложится и на GPU.

Очевидным решением будет разбить сигнал на равные интервалы, посчитать, например, средние значения на этих интервалах и полученный набор (средних) использовать для отображения вейвформы. На деле будем использовать средние и максимальные значения на интервалах — для каждой аудиодорожки будем строить 2 вейвформы одна над другой. Плюс может потребоваться добавлять вейвформы «на лету” (для отображения отфильтрованного сигнала поверх оригинального). Существенным усложнением данного подхода является необходимость изменять масштаб вейформы, а также «перемещаться» по оси времени для выбора нужной области.

Замечу, что чтение сэмплов из аудиофайла и построение вейвформы может занимать продолжительное время. Для примера, на iPhone 5s вейвформа для 10-минутной записи строится порядка 10 секунд. Как известно, хуже ожидания для пользователя может быть только отсутствие информации о том, сколько еще нужно ждать. Поэтому мы решили сделать отрисовку загружающейся вейвформы анимированной, чтобы пользователь видел прогресс в интуитивном формате.

Итого для решения задачи нужно:

  1. Уметь читать одиночные сэмплы аудиодорожки и строить по ним данные для отображения вейвформы

  2. Уметь рисовать несколько вейвформ (с возможностью добавлять вейфвмормы в любой момент).

  3. Иметь возможность рисовать вейвформу “анимированно” по ходу чтения сэмплов.

  4. Добавить возможность масштабировать вейвформу и смещать её по времени.

Существующие решения

Лучшее из найденных решений: EZAudio. В целом интересный фреймворк, но он не совсем подходит для нашей задачи. Например, в нем нет возможности масштабирования графика.

Для отрисовки графика можно было использовать CorePlot. Но в целом это решение показалось громоздким для нашей задачи (также, возможно, потребовалось бы отдельно поработать над оптимизацией отрисовки большого набора значений).

Разработка

Архитектура waveform-модуля

По задаче необходимо уметь отображать не только конечные данные, но и данные в процессе построения. Также напомню о необходимости реализовать зум и перемещение по оси времени. Исходя из этого, было решено сильно отвязать слой UI от данных, которые ему предстоит рендерить.

Smooth waveform UI architecture

Примерно так упрощенно выглядят связи между классами, занятыми в отрисовке вейвформ.

Здесь

  1. Diagram View — контейнер одиночных вейвформ. Он также реализует синхронизацию отрисовки вейвформ, для чего использует CADisplayLink.

  2. Plot View —  класс, занимающийся отрисовкой. Этот класс должен знать только ту информацию, которую необходимо отобразить в данный момент времени, и получать её он будет из соответствующего Plot ViewModel.

  3. Diagram ViewModel — контейнер вью-моделей отдельных вейвформ.

  4. Plot ViewModel — источник данных для одиночной вейвформы. Этот класс будет решать, какие семплы и в каком количестве отдавать на отрисовку, когда это необходимо

Здесь опущены протоколы, через которые на самом деле реализована связь от View к ViewModel. Замечу только, что архитектурой не предусмотрено обратных связей от ViewModel к View.

Благодаря такому подходу мы получили возможность оптимизировать построение данных для вейвформы и отрисовку вейвформы независимо друг от друга. Также это дало возможность внести серьезные изменения в построение данных без воздействия на слой View. Но обо всем по порядку.

Чтение сэмплов

Для построения вейвформы необходимо получить несжатые семплы аудиодорожки. Сделать это можно с помощью инструментов, предоставляемых фреймворками AVFoundation и CoreMedia.

Чтение данных целиком представлено в классе:

[gist id=c24531dfb4adb8ef7b60a67f6a1bd2b2 bump=2]

Не буду очень подробно останавливаться на этом коде. Скажу только, что перед началом чтения сэмплов можно узнать параметры аудиозаписи, такие как частота дискретизации и количество каналов (функция readAudioFormat), и использовать их при чтении. Но можно и пропустить этот момент и при настройке чтения использовать частоту, равную 44100 Гц, и читать по двум каналам. Глубину кодирования придется задать вручную (16 бит).

Также отмечу, что чтение сэмплов может занимать продолжительное время. Для видео в несколько минут время чтения будет порядка нескольких секунд. Поэтому алгоритм нужно использовать в фоновом потоке.

Обработка сэмплов

Следует помнить, что алгоритм чтения сэмплов, вообще говоря, выдает блоки разных размеров. В этом случае нам нужно последовательно обходить каждый новый блок сэмплов, и по ходу рассчитывать требуемые для построения вейвформ значения.

[gist id=fced3ba8b6fa654863bca2a16150b388 bump=2]

Swift vs ObjC. Что лучше?

Поскольку приложение уже было написано на Objective-C, начинать писать новый модуль на Swift было неочевидным решением. Например, что касается построения данных для графиков, требовалось убедиться, что Swift не создаст проблем с производительностью. Мы провели проверку аналогичных алгоритмов, написанных на обоих языках и использующих аналогичные типы данных (например, если в Swift использовался тип UnsafePointer<Double>, то в алгоритме на Objective-C — массив double*).

Результаты были следующие:

Данные по работе алгоритмов чтения аудиосемплов и построения данных для графика.

iPhone 5

Swift (Whole-Module-Optimization):  20.1 c

Objective-C (-Ofast):  41.4 c

iPhone 6s

Swift (Whole-Module-Optimization):  5.1 c

Objective-C (-Ofast):  4.1 c

Тесты проводились и на других моделях iPhone. Результаты показали сильные расхождения на моделях с 32-разрядными процессорами и небольшие — на 64-разрядных моделях. (Для дальнейшей работы над оптимизацией в основном использовался iPhone 5s).

Как видим, большой разницы нет. При этом некоторые особенности Swift очень помогли в решении этой задачи. Про это чуть ниже.

Масштабирование вейвформы

В первоначальном варианте для зума использовалась возможность чтения сэмплов из произвольного отрезка аудиодорожки. См. свойство объекта AVAssetReader

[gist id=d88bb2fad2ea196214e27ea7277691b5 bump=2]

И при каждом зуме графика чтение начиналось снова.

pre_mov_crop

Вариант вполне рабочий, однако пересчет данных для отрезка требует примерно того же времени, что и расчет для всего аудиофайла: время порядка нескольких секунд (см. данные выше).

Рефакторинг обработки сэмплов

Для реализации плавного зума было решено попробовать другой подход. При чтении сэмплов можно строить данные для графика сразу для нескольких масштабов, выдавать на начальном этапе данные для исходного масштаба, а остальные наборы данных хранить в локальном хранилище (или в оперативной памяти) и отдавать их в UI по мере необходимости (а при зуме подменять один набор данных другим). Как мы уже упомянули, наша архитектура позволяет сделать это без необходимости изменений в UI.

Всю обработку блоков сэмплов мы обернули в отдельный класс, в котором реализовали хранилище для обработанных данных и возможность гибко менять логику обработки.

В качестве массива обработанных данных был использован UnsafeMutablePointer<T>, который по своей сути является аналогом массива на языке C (см. документацию (англ.): Структура указателя UnsafeMutablePointer, Взаимодействие с API языка C). Пример реализации брался отсюда.

[gist id=c0fb3081b149674d659bb7d8eb39628c bump=2]

Примечание. Здесь опущен очень важный момент: этот класс будет использоваться параллельно в разных потоках (запись в бэкграунде, чтение в главном потоке). В Swift встроенных средств для работы с многопоточностью нет, так что атомарное поведение буфера придется реализовать вручную (например, вот так). В нашем случае, при использовании Array<T> вместо UnsafeMutablePointer<T> и без синхронизации мы периодически сталкивались с выходом за границы массива. С UnsafeMutablePointer<T> такая проблема не возникала.

Про числовые типы

Пару слов скажу про ассоциированный тип T в классе Channel<T>.

Напомним, что для реализации зума было решено обрабатывать сэмплы и строить данные сразу для нескольких масштабов вейвформы. Поэтому нужно было добиться не только высокой производительности, но и эффективности при работе с памятью. Для нашей задачи для экономии памяти достаточно хранить сэмплы в Int16, а там, где нужно — явно приводить типы (благо стандартные числовые типы в Swift легко приводятся один к другому). Однако задача была решена в чуть более общем виде — то есть, с возможностью выбирать числовой тип для хранения данных. Для нашей задачи мы создали протокол для работы с числами любого типа (в Swift нет единого числового типа).

[gist id=f7d0a71ff6411e9a446a4b862493df9f bump=2]

Теперь в коде класса Channel можно указать следующее:

[gist id=b32e3a723349be40c0e87f4fa6338523 bump=2]

Параллельные расчеты для нескольких масштабов

Теперь можно в класс Channel добавить обработку сэмплов (например, нахождение максимумов).

[gist id=4a1fe32f34847c3dda309a64c0f73c03 bump=2]

Обработка (сразу по нескольким каналам, для нескольких масштабов) будет выглядеть примерно так.

[gist id=64ab536114e0e2fd9c89acfb6bdab5b2 bump=2]

Для получения конкретных данных будем запрашивать нужный канал. При зуме будем вызывать метод reset, что будет приводить к подмене текущего канала (при определенном масштабе).

По такому же принципу можно добавить вычисление средних значений. А чтобы все это работало вместе, можно создать один абстрактный класс Channel и 2 наследника MaxChannel и AvgChannel (для максимумов и средних значений, соответственно).

После всех манипуляций зум выглядит так:

post_mov_crop

Оптимизация

Построение набора данных для вейвформы

В общем, этого достаточно для решения задачи, однако мы попробовали оптимизировать алгоритм. Что касается Swift, если вам требуется высокая производительность того или иного алгоритма, то одними из главных препятствий станут ARC и динамическая диспетчеризация. Вообще говоря, в Swift есть ряд возможностей помочь компилятору опустить некоторые проверки. Здесь мы рассмотрим один из подходов.

Для оптимизации было сделано следующее:

  1. Создали базовый класс, который инкапсулирует логику обработки сэмплов.

  2. В классе-канале создали свойство этого типа.

  3. В классе-логике создали слабую ссылку на класс-канал.

  4. Отнаследовали от этого типа конкретные обработчики (для средних и для максимумов).

  5. Сам класс-канал сделали ненаследуемым (с помощью ключевого слова final).

  6. Включили оптимизацию Whole-Module-Optimization.

Про 2 последних пункта читайте здесь.

Режим Whole-Module-Optimization включается в настройках проекта.

whole-module-optimization

 

[gist id=8e5220ed977713d51aa4831957bff202 bump=2]

Результаты сравнения скорости работы алгоритма (продолжительность видеоролика 10 мин):

В случае с наследованием каналов и логикой обработки внутри каналов время построения данных: 27.95 с

Для варианта с “внешней” логикой — 6.66 с

В конечном итоге, буфер также решено было спрятать внутрь отдельного класса для того, чтобы подружить класс Channel с ObjC (ObjC не умеет работать с обобщенными типами Swift).

Графика

Немного расскажем про работу с графикой.

По задаче требуется уметь рисовать большое количество вертикальных линий.

Исходный вариант

Изначальный вариант базировался на использовании CAShapeLayer и отрисовки с помощью линии обводки (stroke).

[gist id=dd1f08d4869e3b32ae487328b5361664 bump=2]

Время отрисовки для 2000 точек на каждую из 2 вейвформ, на iPhone 5s: 18 с

Вообще говоря, это рабочий вариант, однако для большого количества точек (порядка 1000), мы получали задержки при отрисовке кадров.

Вариант с оптимизацией №1  (построение CGPath без CGPathMoveToPoint)

Для начала избавились от использования функции CGPathMoveToPoint и там, где нужно, стали просто добавлять лишнюю линию вместо перемещения между точками.

[gist id=193e43e94307ff4295351966c96286e5 bump=2]

Время отрисовки для тех же исходных данных: 14.8 с. 

Вариант с оптимизацией №2 (stroke против fill)

Вейвформы у нас довольно простые: строим линию от середины вверх в соответствии с уровнем сэмпла, повторяем симметрично вниз, возвращаемся на середину, переходим к следующему значению. Это позволило совсем отказаться от stroke и строить линии, закрашивая нужные области (fill). При этом по прежнему рисуем замкнутую фигуру.

[gist id=167b715f0ec665fc793f12fc46c598b8 bump=2]

В этом случае получаем:  12.4 с.

Прирост скорости небольшой и заметен только на относительно старых устройствах (до iPhone 5s включительно).

Вариант с оптимизацией №3 (drawRect против CAShapeLayer)

Также попробовали вместо CAShapeLayer использовать отрисовку того же самого CGPath в drawRect (алгоритм построения CGPath как варианте № 1)

[gist id=8e7a4b4acc44b6bb1725bdb8a027e6b6 bump=2]

Обращаем ваше внимание на то, что необходимо правильно выставить толщину линии, исходя из параметров экрана. Иначе преимущества в скорости вы не получите — отрисовка получится прерывистой и медленной. Также заметим, что здесь отключено ненужное сглаживание.

Результат теста: 9.46 c. 

В итоге, в результате оптимизации построения CGPath и переходе на отрисовку в drawRect(), можно ускорить построение вейвформы почти в 2 раза. Полученного результата уже достаточно для плавной отрисовки нашей вейвформы.

Вместо заключения

  1. Как видим, Swift вполне подходит для задач подобного уровня.
  2. Оптимизация алгоритмов на Swift – это одна большая тема, которую мы еще постараемся осветить.
  3. Для достижения оптимальных параметров отрисовки вашей графики не обязательно переходить на OpenGL или Metal.
  4. MVVM-подобный подход к построению архитектуры с разделением ответственностей View и ViewModel хорошо подходит для реализации  сложного интерактивного интерфейса.

Целиком проект с демоприложением можно найти на GitHub по ссылке.

Плеер плейлистов для iOS

DVPlaylistPlayer Title

Одной из основных частей приложения, так или иначе работающего с видео, является видеоплеер. Если вы хотя бы раз имели дело с интеграцией плеера в своё приложение, то наверняка знаете, что его настройка и кастомизация занимает определённое время. Также мы готовы поспорить, что чаще всего вам не нужен был плеер, который играл бы только одно видео за всё время работы с приложением. В большинстве случаев вы бы хотели использовать плейлисты. В этой статье мы хотим рассказать вам о разработанном нами открытом компоненте, который мы используем для воспроизведения видео в нашем проекте Together. Речь пойдёт о плеере плейлистов DVPlaylistPlayer.
Continue reading

AirPlay и HDMI

Если вы разработчик игровых или мультимедиа-приложений для iOS, тогда вы просто не сможете обойтись без технологии AirPlay, которая в данной области стала уже почти стандартом. В самом деле, что может быть лучше для пользователя, чем наблюдать любимые видео или фотографии на большом экране плазменного ТВ или играть на нем в современные игры, при этом держа iPhone/iPad/iPod в руках в качестве мультитач-джойстика? AirPlay предназначен именно для этого. Continue reading

Возможности Screen Capture в iOS

Функция Screen Capture, или, попросту говоря, «снятие изображения экрана», в мобильном приложении представляет собой мощный вспомогательный инструмент и может использоваться в самых разнообразных задачах. В этой статье мы подробно рассмотрим механизмы, доступные для реализации данного функционала. Continue reading

Защищенная доставка и воспроизведение видео в iOS: поддержка DRM

Протокол HTTP Live Streaming (HTTP LS) рекомендуется и всячески продвигается компанией Apple в качестве наиболее оптимального решения для онлайн-доставки видеоконтента на мобильные устройства. Но ни сам этот протокол, ни средства для работы с ним, имеющиеся в API операционной системы, не реализуют полноценную поддержку систем DRM. Новая версия системы Adobe Access 4.0 добавляет на iOS-устройства эту недостающую функциональность. Continue reading

Разработка крупных JavaScript приложений

Создание интерактивных Интернет сервисов http://tutrostov.ru/  подразумевает широкое применение JavaScript при их разработке. Если 10 лет назад этот язык имел скудным функционал, но в данный момент  JS существенно преобразился. Все больше логики сервисов переносится в клиентскую часть. В статье мы поделимся с вами нашим опытом проектирования и реализации крупных веб приложений. Continue reading

HTML5 <video> аналитика

Если Вы работаете в области online-video сервисов, то наверняка задумывались об аналитике просмотров видео пользователями. Для платформы OSMF (фреймворк, написанный на Flash технологии) существует плагин GTrackPlugin осуществляющий сборку статистики по просмотру контента пользователем. Также можно использовать нашу собственную разработку OSMF Video Analytics.

Однако если сервис основан на HTML5 видео, то необходимо решение, реализующее аналитику просмотров видео для данной технологии. Реализация данного плагина основана на работе с HTML5 Media API. (Подробнее о работе с данным API Вы моете прочитать в статье о реализации HTML5 плеера). Continue reading

Разработка HTML5 плеера

HTML5 JS Player

С каждым годом медиа технологии все больше http://tutbelgorod.ru/  и больше входят в жизнь каждого человека. Глобальная сеть также подвержена данной тенденции, о чем свидетельствуют  успех и популярность таких сервисов как YouTube. Но работа подобных систем невозможна без технологий, позволяющих реализовать воспроизведение видео контента для пользователя.

Классическим подходом для реализации проигрывателя видео является использование технологии Flash.  Пожалуй, сложно найти более распространенный плагин для веб-браузера, чем Flash Player. Однако стоит заметить, что слабым местом подобного подхода является необходимость установки Flash plugin’а. А для многих устройств он вообще отсутствует. К примеру, настолько популярные iPhone и  iPad не имеют возможности запускать Flash-приложения в браузере. Выходом из подобной ситуации является использование HTML5. Continue reading

Разработка AIR видео приложений под Android

android video apps
Сегодня смартфоны и планшеты стали неотъемлемой  частью нашей повседневной жизни. Люди взаимодействуют с этими устройствами много раз за день и проводят много времени за этим занятием. Мобильные и планшетные приложения стали важными каналами распространения контента. Запуск практически любого крупного сервиса включает в себя специализированное приложение. Сегментация мобильных приложений сильно увеличивает затраты на реализацию мобильной стратегии. Adobe AIR позволяет снизить затраты на запуск кроссплатформенных сервисов, разработанное один раз приложение проще адаптировать под разные устройства, экраны (кроме того, хороших Flash разработчиков гораздо больше чем хороших iOS и Android специалистов). В этой статье мы расскажем про некоторые особенности разработки мобильных приложений при помощи Adobe AIR. Continue reading