Как стать автором
Обновить
2955.07
RUVDS.com
VDS/VPS-хостинг. Скидка 15% по коду HABR15

Для чего я написал собственный аудиопроигрыватель

Уровень сложностиСредний
Время на прочтение12 мин
Количество просмотров3.4K
Автор оригинала: Oleg Pustovit

Как и у многих, у меня накопилось слишком много подписок: некоторые у Apple (iCloud, Apple Music), другие потерялись на разных платформах (например, на Netflix — я и забыл, что всё ещё плачу за него). На самом деле, я регулярно пользовался Apple Music (а ранее Spotify), но потоковая музыка оказалась больше удобством, чем необходимостью. При наличии тщательно подобранной локальной библиотеки я ничего особо не потерял.

Поначалу я думал, что просто продолжу использовать iCloud Music Library для синхронизации музыки между устройствами, но после отмены подписки на Apple Music синхронизация перестала работать. Оказалось, за эту функцию нужно платить. Строго говоря, её можно вернуть при помощи iTunes Match (24,99 $ в год). Match просто хранит AAC-копии с битрейтом 256 кбит/с; ваши исходные файлы остаются на месте, если вы не захотите удалить их. На современных Mac всё это делается в приложении Music. При отсутствии подписок синхронизация с облаком отключается и остаётся лишь синхронизация по кабелю/Wi-Fi.

Раздосадованный ограниченностью возможностей, я решил взяться за разработку. Если уж я купил вычислительное устройство (в данном случае iPhone), то что мешает мне просто создать необходимое мне при помощи кода? В этой статье я расскажу о своих разочарованиях по пути к реализации базовой функциональности музыкального плеера: загрузки аудиофайлов, их упорядочивания и воспроизведения. Но больше всего мне хотелось напомнить себе, что это просто компьютер, и у меня должна быть возможность заставить его выполнять нужные мне задачи.

Что предлагает сегодня Apple (и другие компании)


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

▍ Встроенные приложения Apple


Строго говоря, Apple позволяет проигрывать музыку непосредственно из iCloud при помощи приложения Files, но его функциональность не предназначена для прослушивания музыки. Ей недостаёт обязательных возможностей: управления плейлистами, сортировки по метаданным и очередей воспроизведения. Хоть оно и поддерживает воспроизведение музыки, но очень ограниченно и не особо удобно для пользователя.

▍ Сторонние приложения


Я зашёл в App Store, чтобы найти хорошие приложения, решающие мою проблему. Хоть их там было много, большая их часть приобреталась по подписке — сомнительная модель для приложения, которое просто воспроизводит файлы, уже имеющиеся у пользователя. Мне понравилось одно приложение — Doppler. Я тестировал его в течение пробного периода, но его UX построен на основе управления альбомами. Поиск работал не очень хорошо, а функции импорта из iCloud оказались медленными и их неудобно было использовать при большом количестве вложенных папок. Плюсом было то, что платить за него нужно только раз.

Переключаемся в режим творца


Учитывая всё вышесказанное, я решил создать собственный идеальный проигрыватель музыки, решающий все мои проблемы:

  • Гибкий полнотекстовый поиск по папкам iCloud, чтобы можно было быстро выбирать и импортировать папки с музыкой или отдельные файлы.
  • Функциональность управления музыкой как минимум на уровне официального приложения Music: очередь, управление плейлистами, сортировка по альбомам и так далее.
  • Привычный и удобный интерфейс.

▍ Пробуем сначала React Native


Поначалу я старался избегать работать со Swift из-за предыдущего опыта. Несколько лет назад мне понравился его синтаксис (он казался похожим на TypeScript) и безопасность по памяти в духе Rust; но в то время в нём отсутствовали нативные async/await, поэтому написание конкурентного кода казалось по сравнению с работой на Go или JS/TS перегруженным бойлерплейтом и неудобным. Этот опыт меня разочаровал, поэтому когда я приступил к своему проекту, то поначалу решил использовать нечто более знакомое.

Решил я начать с React Native или Expo, понадеявшись воспользоваться своим опытом веб-разработки, а затем подключить UI проигрывателя из готовых шаблонов. В создании UI воспроизведения не было ничего сложного; существует множество опенсорсных примеров и туториалов по созданию красивых проигрывателей под мои потребности. Я выбрал готовый шаблонный проект Джонаты Стурбы, потому что мне показалось, что в нём есть все фичи, необходимые для моего приложения.


Серьёзными проблемами стали доступ к файловой системе и синхронизация облачных файлов: библиотеки наподобие expo-filesystem поддерживали базовый выбор файлов, но рекурсивный обход папок с глубокой вложенностью iCloud часто завершался неудачей или даже приводил к вылету приложения. Стало очевидно, что решение на основе JavaScript добавляло больше сложности, чем при работе с нативными API Apple, пусть и последние требовали гораздо более вдумчивого изучения.

Песочница iOS не позволяет приложениям считывать файлы без разрешения пользователя, поэтому React Native не мог надёжным образом получать доступ к внешним папкам. Переход на Swift повысил степень контроля за доступом к файлам в iCloud и разрешениями песочницы.

▍ Переход на SwiftUI


Я выбрал SwiftUI, а не UIKit или сториборды, потому что стремился к чистому и декларативному слою UI, который бы не мешал мне при работе с логикой предметной области и синхронизацией данных. Благодаря современным фичам наподобие async/await и интеграции с Swift Actors мне было легче управлять потоком данных и конкурентностью. Кроме того, SwiftUI упростил процесс структурирования приложения на изолированные компоненты ViewModel, что, в свою очередь, помогло мне добиться более качественных результатов от LLM (OpenAI o1 и DeepSeek). LLM могут генерировать чистый код UI или код привязки данных, не добавляя при этом запутанных взаимозависимостей.

Архитектура приложения и модель данных


Давайте разберём архитектуру созданного мной приложения: для постоянного хранения данных я использовал SQLite, в качестве архитектуры приложения выбрал простое серверное приложение. Я отказался от CoreData, потому что мне требовался полный контроль за схемой, сырыми запросами и особенно за полнотекстовым поиском. Встроенная в SQLite поддержка FTS5 позволила мне добавить возможность быстрого нечёткого поиска без необходимости подтягивания внешних тяжёлых поисковых движков или создания собственного слоя индексирования.

▍ Три основных экрана


Приложение состоит из трёх экранов/режимов:

  1. Импорт библиотеки. На этом экране добавляется папка библиотеки в iCloud. Приложение сканирует каждую папку в поисках аудиофайлов и вставляет каждый путь в базу данных SQLite. Благодаря этому можно обеспечить полную гибкость поиска, добавления папок и подпапок. Нативное средство выбора файлов Apple очень неудобно; невозможно выбрать за один раз несколько папок, по которым выполнялся бы поиск ключевого слова, а затем группу файлов — оно просто для этого не предназначено.
  2. Управление библиотекой. На этом экране можно выполнять операции с добавленными композициями и упорядочивать плейлисты. По большей мере я просто повторил то, что Apple сделала в своём приложении Music, и этого было вполне достаточно для моих потребностей.
  3. Плеер и воспроизведение. Эта часть приложения работает с управлением очередью (повтор, перемешивание и так далее), с функциями воспроизведения, паузы и перехода к следующей композиции.

Вот простая диаграмма пути пользователя:


Путь пользователя на практике: когда приложение запускается с пустой библиотекой, оно открывается на вкладке Sync и отображает большую кнопку «Add iCloud Source». После выбора папки экран Sync отображает полосу прогресса, пока приложение обходит дерево. После завершения индексирования приложение переключается на вкладку Library, на первом экране которой находится список Плейлисты / Исполнители / Альбомы / Композиции. После выбора любого из списков можно нажать на дорожку, и в нижней части экрана откроется мини-плеер; если нажать на эту мини-панель, то откроется полноэкранный плеер с функциями перемешивания, повтора, изменения порядка очереди и настройки громкости. Если свайпнуть или коснуться значка закрытия, то снова откроется режим Library, а воспроизведение продолжится. Если пользователю понадобится больше музыки, он может перейти в окно Sync, нажать на «+» в панели навигации, после чего сервис импорта в фоновом режиме добавит новые композиции без необходимости перезагрузки.

▍ Слой логики в стиле бэкенда


У меня есть опыт в веб- и облачной разработке, я выпускал много серверного кода, работая в стартапах, поэтому выбрал для мобильного приложения архитектуру в стиле бэкенда. Весь слой предметной области/логики был отделён от слоя представления (View) и модели представления (View-Model), потому что мне нужно было реализовать синхронизацию с облаком и парсинг метаданных, имея при этом чистый доступ к данным в базе данных SQLite. Вот приблизительная диаграмма слоёв архитектуры:


Как слои общаются друг с другом: SQLite находится в самом низу и хранит сырые строки композиций и индексы FTS. Далее репозитории обёртывают базу данных и раскрывают асинхронные API. Поверх всего этого находятся мои акторы предметной области, акторы Swift, владеющие всеми бизнес-правилами (импорт, поиск, логика очередей), поэтому изменения состояний остаются потокобезопасными. ViewModel подписываются на акторов, преобразуют данные в готовые для UI struct, а представления SwiftUI просто рендерят всё, что получают. Никто в явном виде не пересекает границы слоёв, благодаря чему синхронизация с iCloud, воспроизведение и UI удобно разделены.

Реализация полнотекстового поиска с помощью SQLite


Как говорилось выше, мне повезло, что можно импортировать версию SQLite с возможностями FTS: начиная примерно с iOS 11 она доступна «из коробки» без дополнительной настройки. Это упростило интеграцию нечёткого поиска в мою библиотеку музыки без сторонних зависимостей. Кроме того, я использовал библиотеку SQLite.swift для регулярных запросов (её можно использовать в качестве построителя запросов с безопасностью времени компиляции); однако для FTS-запросов мне пришлось довольствоваться регулярными выражениями в операторах SQL.

В конечном итоге, расширение SQLite FTS5 оказалось одним из самых ценных элементов архитектуры. Оно позволяет мне выполнять запросы имён файлов и таких метаданных, как имя исполнителя, альбом и название композиции, без дополнительной инфраструктуры индексирования.

▍ Настройка таблиц FTS


Область Актор Swift / репозиторий Таблица FTS5 Индексируемые столбцы
Композиции библиотеки SQLiteSongRepository songs_fts artist, title, album, albumArtist
Исходные пути браузера SQLiteSourcePathSearchRepository source_paths_fts fullPath, fileName

Я использовал две таблицы FTS5: одну для индексированных композиций (по исполнителю/названию/альбому), другую для путей файлов во время импорта папок. Обе таблицы находятся рядом с первичными строками в обычных таблицах B-деревьев (songs, source_paths). FTS в UI доступно только для чтения; все операции записи происходят внутри репозиториев, поэтому никакие данные просочиться не могут.

▍ Создание поискового индекса


Встроенное в SQLite FTS5 упрощает выполнение быстрых операций поиска. Вот использованное мной простое определение таблицы:

try db.execute("""
CREATE VIRTUAL TABLE IF NOT EXISTS songs_fts USING fts5(
  songId UNINDEXED,
  artist, title, album, albumArtist,
  tokenize='unicode61'
);
""")

Я использовал токенизатор unicode61, чтобы обрабатывался широкий спектр символов. Ключи, по которым нельзя искать, помечены как UNINDEXED, чтобы они не увеличивали размер словаря терминов.

▍ Надёжное обновление данных


Чтобы система была простой и безопасной, я обернул обновление и вставку данных в транзакции. Это гарантирует отсутствие рассинхронизации поискового индекса даже в случае вылета или прерывания работы приложения.

func upsertSong(_ song: Song) async throws {
    db.transaction {
        // вставка или обновление основных данных композиции
        // вставка или обновление данных поискового индекса
    }
}

▍ Запросы с нечётким поиском


Чтобы обеспечить возможность удобного поиска, я добавил автоматическую поддержку подстановочных символов. Если пользователь введёт «lumine», то система выполнит поиск по «lumine*», сразу же выдавая результаты даже для частичных запросов.

Кроме того, я использую встроенное интеллектуальное ранжирование SQLite (bm25), чтобы возвращать наиболее релевантные результаты без повышения сложности:

SELECT s.*
FROM songs s JOIN songs_fts fts ON s.id = fts.songId
WHERE songs_fts MATCH ?
ORDER BY bm25(songs_fts)
LIMIT ? OFFSET ?;

В целом, использование сырого SQLite обеспечило мне необходимую гибкость: предсказуемую схему, доступ «local-first» и мощный полнотекстовый поиск, не добавляя при этом сетевых зависимостей или внешних сервисов. Такой подход идеален для разработки приложения, в первую очередь используемого в личных целях и в офлайне.

Работа с файлами и закладками iOS


В iOS приложения могут хранить постоянные закладки местоположений файлов, однако закладки с безопасным ограничением области видимости, обеспечивающие расширенный доступ снаружи песочницы приложения, доступны только в macOS. Можно использовать в iOS обычные закладки, чтобы запоминать пути к файлам и запрашивать повторный доступ при помощи средства выбора документов, но гарантий постоянной сохранности такого доступа нет. См. документацию Apple по закладкам.

Чтобы решить эту проблему, я реализовал страховочный механизм, копирующий файлы в собственный контейнер приложения внутри песочницы. Это позволяет избежать использования закладок с негарантированным сроком жизни, который может без предупреждения прекратиться, если iOS сбросит разрешения. Благодаря копированию файлов заранее в фоновом режиме, пока закладка ещё валидна, не возникает риска, что мы будем ссылаться на невалидный аудиофайл.

Такая методика повышает и скорость индексирования. Можно один раз отсканировать структуру папок (пока доступ активен), импортировать только релевантные аудиофайлы и безопасно выполнять обход глубоко вложенных папок. Но для меня остаётся нерешённой проблемой надёжное воспроизведение отдельных аудиофайлов из внешних адресов. Это подчёркивает, насколько плохо поддерживается подобный сценарий использования, даже для нативных приложений, и насколько по-прежнему сложно надёжно обрабатывать доступ к файлам в iOS.

Создание воспроизведения и UI


▍ Парсинг метаданных


Для парсинга метаданных из аудиофайлов я воспользовался фреймворком Apple AVFoundation и конкретно классом AVURLAsset, который позволяет изучать метаданные медиафайлов (название, альбом, имя исполнителя и так далее). Хотя парсинг метаданных выполняется нативным SDK, некоторые поля, например, номера треков, приходится вручную искать в тегах ID3. Примеры мне пришлось искать при помощи поиска GitHub, потому что в официальной документации не рассматривались пограничные случаи.

▍ Воспроизведение аудио с помощью AVFoundation


После индексирования библиотеки реализация аудиопроигрывателя оказалась довольно простым процессом: достаточно инициализировать экземпляр AVAudioPlayer и запустить аудио. Кроме того, для повышения удобства пользования (воспроизведения музыки из центра управления) мне пришлось реализовать протокол AVAudioPlayerDelegate, а также подключиться к MPRemoteCommandCenter Apple, который позволяет разработчикам реагировать на сигналы элементов управления воспроизведением на системном уровне.

Размышления: Apple, забор для разработчиков и будущее


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

▍ Плохое


Ограничения Xcode по-прежнему раздражают. Превью SwiftUI в реальном времени — это хороший шаг вперёд, но в целом процесс разработки находится на том уровне, который обеспечивал Flutter пять лет назад: тесная интеграция с VSCode, перезагрузки симулятора в реальном времени и привычные инструменты отладки.

Недостаточная гибкость редактора. Для настройки поддержки Language Server Protocol (LSP) для Swift в Neovim или VSCode требуется дополнительный инструментарий наподобие xcode-build-server, но всё равно удобство не дотягивает до уровня экосистем, в первую очередь рассчитанных на веб.

Некоторые части SDK Apple по-прежнему находятся в мире Objective-C. Например, поиск файлов Spotlight доступен только через NSMetadataQuery, использующий Key-Value Observing (KVO) и строковые ключи; удобной для Swift обёртки пока не существует. Документация часто не очень подробна, что усложняет процесс обучения.

Декларативный UI системы SwiftUI прекрасен, но для отладки взаимодействий с iCloud по-прежнему требуются создаваемые вручную моки. Превью SwiftUI неспособны эмулировать поведения работающих с iCloud приложений полностью, поэтому необходимо создавать моки взаимодействий с облаком вручную, а это несущественная, но заметная проблема.

▍ Хорошее


Async/await. Наконец-то можно писать ограниченный вводом-выводом конкурентный код, как императивный, без раздражающих обратных вызовов. Это огромный плюс, и мне очень нравится, что можно легко писать даже синхронизированный код в акторах и вызывать его, как это делается в экосистемах JavaScript.

Изобилие нативных библиотек. Да, вы не ограничены опенсорсными привязками, как в экосистемах React Native/Flutter. Здесь гораздо больше свободы в разработке чего-то «более серьёзного», чем замена веб-сайта компании/продукта. У многих API Apple есть примеры, благодаря чему их легко осваивать.

Сам SwiftUI. Да, разработка UI в стиле React обеспечивает мне повышенную продуктивность и даёт пространство для исследований. Отлично, что Apple реализовала её.

▍ Подведём итог: разработка должна быть проще


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

Однако разработчики быстро осознали, что теперь они не могут просто разворачивать приложения на собственных устройствах: без сертификата разработчика приложения будут работать всего 7 дней, после чего придётся их пересобирать или же заплатить Apple 99$ для участия в программе разработки.

Даже после принятия DMA Act в ЕС sideloading по-прежнему открыт не полностью. Пользователи в ЕС теперь могут устанавливать приложения из сторонних маркетплейсов непосредственно с сайта разработчика, но только если разработчик зарегистрирован в программе Apple (99$ в год) и согласился на Apple Alternative Terms. Для личного использования семидневное ограничение на сборку по-прежнему не снято.

Это не имеет никакого смысла. Инновационная технологическая компания активно создаёт условия, препятствующие демократизации разработки приложений. Даже прогрессивные веб-приложения (PWA) сталкиваются в iOS с существенными ограничениями: после обновлений 16-18.x Apple PWA в iOS по-прежнему работают внутри песочницы Safari. У них появились WebGL2 и web-push, но не появились Web Bluetooth/USB/NFC, Background Sync, а гарантированное хранилище по-прежнему ограничено примерно 50 мегабайтами. WebGL выполняется через Metal-шим, поэтому в реальности частота кадров таких приложений отстаёт от показателей нативных приложений Metal; этого вполне достаточно для UI, но не для 3D-игр уровня AAA.

Сегодня ИИ снизил сложность разработки современного ПО, позволив любому попробовать изучать неизвестные технологии, предоставив все необходимые знания в доступной форме. Мы видим, что веб-разработка всё больше начинает интересовать людей без технических знаний, которые могут реализовывать свои идеи без глубоких знаний и кучи технологий. Но в случае с мобильными приложениями приходится играть по искусственно придуманным правилам. Даже если вы создаёте что-то сами и для себя, последнее слово всё равно остаётся за Apple, а до этого ваше приложение не может работать больше недели. Та же самая компания, которая когда-то дала новые возможности независимым разработчикам, теперь накладывает серьёзные ограничения, мешающие разработке личных приложений и их распространению. ИИ упростил создание новых инструментов, но только не для iOS, где вход по-прежнему только по пропускам.

Ссылки



Telegram-канал со скидками, розыгрышами призов и новостями IT 💻
Теги:
Хабы:
+29
Комментарии5

Публикации

Информация

Сайт
ruvds.com
Дата регистрации
Дата основания
Численность
11–30 человек
Местоположение
Россия
Представитель
ruvds