Следующим интересным мне субпакетом в биндингах GtkD оказался gtk-d:gstreamer, реализующий удобные объектные обёртки для мультимедийного фреймворка GStreamer.
Фреймворк достаточно популярен, используется повсеместно для самых разных мультимедийных приложений, что делает знакомство с ним полезным, а в некоторых случаях даже обязательным. Тулкит JavaFX, встречающийся на страницах этого блога, его также использует для мультимедиа. Ранее я уже сталкивался с его пайплайнами, однако без удобных объектных биндингов это знакомство нельзя было назвать приятным.
Из всех разнообразных мультимедийных проектов наиболее простой и экономной по времени затеей выглядит аудиоплеер: относительная простота устройства и логики, отсутствие критичного функционала, большое количество существующих аналогов, делает его наилучшим вариантом для эксперимента. С другой стороны, тут появляется и основной риск из-за работы с файлами из внешнего мира и сетью - уязвимости в т.ч. 0-day.
Всплывают они периодически, можно вспомнить, например, прошлогодний парад CVE-2021-3522, CVE-2021-3498, CVE-2021-3497, CVE-2021-3185 и прочие. Из-за системного стека уязвимости там будут и есть, их вскрытие лишь вопрос времени, что следует учитывать при любом потенциальном взаимодействии с вредоносным кодом. С другой стороны, дополнительные средства защиты самой операционной системы, относительная популярность фреймворка и сложность эксплуатации некоторых уязвимостей уменьшают какую-то часть рисков, что делает эксперимент вполне допустимым.
В качестве альтернативы подробной оценке разных функциональных и нефункциональных требований, как это было в статье о текстовом редакторе, можно пойти и другим путём, используя более общий, но и более фундаментальный закон - принцип Парето.
В трактовке Википедии он звучит как "20% усилий дают 80% результата, а остальные 80% усилий - лишь 20% результата". Изначально я не придавал ему большого значения, ориентируясь на более специализированные аналитические инструменты, однако отложенные проблемы и архитектурные ошибки быстро привели к пониманию важности и фундаментальности этого закона. Вероятно, в нём могут быть определённые пересечения с бритвой Оккама и стремлением к редукционизму.
Можно попытаться поискать эти 20% определяющего функционала для плеера, которые бы сделали его более-менее полезной программой, решая 80% задач, когда как остальные огромные списки возможностей плееров, начиная от загрузки текстов песен и заканчивая редактированием самих медиафайлов могут использоваться в общем случае редко, быть заменены специализированными программами (которые часто делают свою работу лучше) или же вообще не пригодиться никогда. Так и может выйти, что 80% функционала обеспечит решение лишь 20% редких и малоиспользуемых задач.
Например, несмотря на огромный стаж в прослушивании самых разных музыкальных жанров и высокую музыкальную привередливость, я не припоминаю активного использования какого-либо сложного функционала проигрывателей, ограничиваясь самым минимумом. Разве что однажды в поисках экзотики переделал советские наушники под разъём TRS и вот там потребовалась сильная коррекция баланса, однако на уровне всей системы, а не приложения.
Для выявления функционала можно сделать декомпозицию модели плеера. Ну ум приходит процессный подход, хорошо подходящим аналогом выглядит SIPOC, с небольшими поправками на музыкальный контекст, конечно же. В самом простом своём варианте его можно представить как:
- Supplier (поставщики) - хранилище музыкальных композиций.
- Input (входы) - файл или адрес, в более общем смысле какой-то URI (вернее, даже URL, учитывая необходимость указания местонахождения ресурса).
- Process (процессы) - обработка мультимедиа-данных.
- Output (выходы) - восприятие музыки сенсорной системой, в простом случае - слух и зрение.
- Customer (заказчики) - слушатель, его субъективные особенности, предпочтения и прочие отличия.
Вся конструкция представляется цепочкой: S->I->P->O->C, которая, вероятно, из-за наличия входа и выхода будет иметь какие-то пересечения с концепцией обратной связи в теории управления и усилителях: сигнал с выхода подаётся в начало цепочки, регулируя настройки музыки, громкость, смену композиций и т.п. Это сходство по аналогии может намекать на основную функцию: максимальное усиление (или даже улучшение) влияния музыки на сенсорные системы, в общем случае - впечатления, эта функция видится наиболее важной.
Поставщиков можно рассмотреть двойственно: с одной стороны, можно подразумевать тех, кто создаёт, распространяет и хранит музыку. Тогда на вход может поступать и самая разная информация, например, дополнительный сведения об исполнителях, их биографии, текстах песен и прочее, намекая на интеграцию со сторонними api, как и встраивание браузера. Этот функционал выглядит дополнительным, но здесь могут быть точки эволюции и расширения, которые для системного языка даются тяжелее. С другой стороны, это можно рассматривать уже как другой параллельный процесс, со своими входами и выходами, ибо возможно удалить его без ущерба для основного функционала плеера.
Отсюда для основного функционала понятие "хранилища" в контексте поставщика выглядит более привлекательно и может быть как локальным (файловая система или же внешний накопитель, сеть), так и внешним - сторонние сервисы, облака и т.п. источники, а значит его можно подвести под единый интерфейс, что пересекается с паттерном Repository и его аналогами.
Насколько в это понятие вписывается, например, интернет-радио? Хранилище само по себе может подразумевать определённые запросы и манипуляции: получение списка, итерации по файлам, когда как онлайн-сервисы или аналоги могут выдавать только поток. Но URL, описывающий местонахождение потока тоже нужно где-то хранить, их может быть несколько (плейлист) или же список может формироваться через сетевые службы, которые скорее и будут хранилищем в данном случае. Но это ограничение live-потока повлияет на api хранилища: получение предыдущего, следующего файлов и т.п. затруднены и выглядит специализированными версиями классов где-то в иерархии пониже.
Заказчиков также можно рассматривать как слушателя или их множество, что намекает на функционал трансляции видео, протоколы DAAP или аналоги для расшаривания медиафайлов, хотя опять-таки функционал выглядит дополнительным, а учитывая риски уязвимостей потребует много времени на качественную и безопасную реализацию. А также можно передать музыку другому коду или приложениям, но случай выглядит более узкоспециализированным, всё-таки на человека влияет не только сама музыка, но и визуализация с интерфейсом, хотя не исключаю, что есть (или появятся) алгоритмы ИИ, которые тоже можно рассматривать как слушателя. Наибольший интерес здесь в интеграции с системой и различными интерфейсами типа MPRIS, в случае которых выходом выглядят события плеера, намекая на простое добавление и удаление слушателей для них.
Что касается процессов, то пайплайны GStreamer очень хорошо укладываются в модель цепочки выше, а значит там тоже есть входы (sink) и выходы (source). Вполне возможно подключить что-то другое, например, микрофон, генератор сигналов или свою реализацию, как и не ограничиваться только звуком или визуализацией на выходе. Но опять-таки поддержка разных форматов файлов, обработка звука, эффекты и т.п. выглядят сильно ситуативными функциями и будут изменяться в зависимости от начала (форматы музыки, распространение и т.д для поставщиков) и конца (личные предпочтения, привычки, особенности для слушателя) всей цепочки, когда как основной интерес - это усиление воздействия музыки.
Однако в части формирования впечатлений начинаются разнообразные влияния дифференциальной психологии: из-за сильного различия людей по разным психологическим характеристикам любые оценки становятся очень субъективными, а сами музыкальные предпочтения сложны и тесно связаны с личностью и внешней средой, формируя отдельное направление музыкальной психологии.
При беглом поиске идей в литературе наиболее полезной скорее оказалась книжка Петрушин - Музыкальная психология. В разделе о взаимодействии ощущений был приведён пример сенсибилизации - повышение чувствительности одного анализатора в процессе его взаимодействия с другим: добавление к слуховому восприятию зрительного может приводить к усилению впечатления от музыки, что намекает на взаимосвязь интерфейса и музыки.
В самом простом случае такое взаимодействие интерфейса и музыки реализуется в существующих плеерах разного рода визуализациями в т.ч. спектрограммы на основе преобразований Фурье. Однако установить нужное количество этих визуализаций уже тяжелее, насколько много их должно быть? Учитывая, что в значительной части плееров их не очень много, а приложение должно быть отличным от конкурентов, то это намёк на увеличение всеми любимых т.н. "свистелок-перделок". Но где-то здесь появляется и верхний предел отвлекающих элементов, после превышения которого они могут начать вредить. Из-за субъективного восприятия музыки установить это можно скорее лишь экспериментально, а часть элементов можно скрывать через настройки или убрать потом насовсем.
Итого, формируется основная идея и концепт - интерфейс должен быть способен оживлять композицию, стимулировать воображение на основе перцептивных образов из графических элементов. Однако это намекает на повышенные требования к дизайну и оформлению, что сложно (и бессмысленно) выполнить в условиях эксперимента.
Можно попробовать сэкономить время уже готовыми gtk-темами, например, взяв тему Dracula и запуская плеер с переменной среды GTK_THEME. Сложнее с иконками, которые зависят от системных, но их не так много, можно и потерпеть. Другая проблема - в предыдущих экспериментах мне не удалось поработать с биндингами к векторной библиотеке Cairo, но в этом случае дизайн получается неконсистентным, а полная отрисовка всего интерфейса займёт много времени. Отсюда выводится компромиссное решение - отрисовка лишь нескольких ключевых элементов интерфейса, например, информационного дисплея, что позволит как поэкспериментировать с биндингами под векторную библиотеку, так и частично сохранить общий стиль интерфейса, хотя опять-таки восприятие его скорее субъективно.
Минимальный функционал для такой работы со звуком: запуск, пауза, громкость, временное перемещение и т.п., так и работы с файлами - это файловый менеджер, плейлисты и список избранного для сохранения названий треков радиостанций, что определяет функциональные требования простым копированием из аналогов.
Основная проблема в таком интерфейсе - это функциональная схожесть файлового менеджера и плейлиста. Файловый менеджер скорее является частным случаем плейлиста, а значит вполне возможно использовать кнопки следующего и предыдущего трека для навигации по дереву файлов. Однако это вводит режимы проигрывателя, как минимум:
- Режим файлового менеджера.
- Режим плейлиста.
Кнопки старта, следующего, предыдущего треков будут применяться то к одному, то к другому, рождая между ними конкуренцию: что будет, если приостановить трек в плейлисте, а потом двойным кликом запустить трек в файловом менеджере? Самое простое - это автоматически переключить режим на файловый менеджер, а значит режимы должны быть чётко обозначены. Пока я остановился на отдельной ToggleButton, как и добавлении\удалении css-класса с цветной границей для подсвечивания активного виджета, хотя выделение элементов только цветом нельзя назвать хорошим вариантом для доступности (accessibility).
Прототип получился каким-то таким:
Есть определённые проблемы с отступами и прочим, но они выглядят допустимыми в обмен на сэкономленное время. Сам Goom красив, очень красив:
Поверхностно пробегусь по основным моментам реализации.
У GStreamer хорошая документация со множеством примеров, несколько вводных статей можно найти на хабре. Построить пайплайн своей мечты можно разными путями, например, через более универсальный playbin, который сам по себе пайплайн, однако хотелось бы вспомнить разные элементы и их взаимодействие друг с другом, так что построение пайплайна вручную более интересно.
Само выстраивание пайплайна достаточно тривиально, основной нюанс скорее в распараллеливании данных для разных целей через tee и очереди.
Для главной визуализации хорошо подходит goom, способный к достаточно красочным и замысловатым узорам.
Для более простых визуализаций и спектрограммы также есть соответствующие плагины audiovisualizers (Bad plugins). Для спектрограммы я остановился на spectrascope. Наиболее понравившийся мне эффект получился в комбинации его с coloreffects с пресетом "heat" и videobalance для более тонкой подстройки цветов под тему интерфейса.
Однако интерес был и в собственной спектрограмме, в реализации которой помог плагин spectrum, обеспечив массивом значений, на основе которых через Cairo можно отрисовать график.
Также я потестировал плагин level, результат которого рисуется по бокам в двух уровнях Gtk.LevelBar: пиковое значение во внешнем и усреднённое значение (RMS) - во внутреннем. При таком построении и склонности пиковых значений постоянно задираться вверх, композиция получается закрытой, окружённой границами со всех сторон. Здесь же пришло в голову более бюджетное решение положить два Gtk.LevelBar горизонтально, заполнить на фулл и изменять им прозрачность наиболее слабо изменяемым значением уровня: не так рябит в глазах, но и есть какая-то динамика на верхней и нижней границе плеера.
Было бы неплохо подстраивать реакцию интерфейса под жанр музыки или какие-то её характеристики, для чего нужна классификация жанров. В природе есть различные проекты, но интеграция, как и реализация такого функционала выглядит скорее занятием для стартапов, чем для простого плеера. Какую-то информацию о жанре можно пытаться получить из тэгов, но это не слишком универсальный подход, к тому же автоматическое изменение цвета индикаторов, дисплея или других элементов выглядит хотя и привлекательным, но противоречивым решением: есть шанс, что меняться оно будет там где не нужно и когда не нужно, зато не срабатывая когда нужно, классика.
Для построения эквалайзера также есть плагины. Я использовал 10-канальный эквалайзер через equalizer-10bands. Нельзя сказать, что мне он очень был нужен, но посмотрев на VLC, один пресет показался полезным - для наушников.
Для вывода изображения был использован самый простой путь - gtksink, однако это требует доустановки пакета в систему (gst-plugin-gtk), при его отсутствии фабрика ElementFactory вернёт null. Соответственно, визуализации нужно отображать в отдельном окне, а также изменять размер видео, что можно сделать через videoscale. Поскольку он работает с Caps и нужна динамическая перестройка размеров видео при изменении окна и gtk-контейнеров, то можно использовать capsfilter, который позволяет настраивать себя свойством GObject.
Для информационного дисплея был нарисован примитивный экранчик и наложен эффект стекла через линейный градиент. Текст можно выводить в cairo.Context напрямую, но Unicode-символы проще вывести через Pango, в модуле pango.PgCairo есть методы для вывода текста в Cairo-контексте.
Информацию о треке можно получить через теги, слушая шину сообщений и события GstMessageType.TAG, что особенно необходимо для онлайн-радио и смены названий проигрываемых треков. Однако иногда парсинг тэгов может не срабатывать, многие проигрыватели берут в этом случае имя трека из файлового пути, не обрезая расширение, я также взял этот способ. В части радиостанций должен быть какой-то функционал сохранения названий или самой композиции. Самый простой вариант - это сохранение названий в обычный текстовый файл.
С другой стороны, при перетаскивании файла в окно программы и добавления в плейлист нужно получить информацию как минимум о длительности. Это можно сделать через GstDiscoverer, но поскольку он убран в gstpbutils, то нужно немножко больше телодвижений, да и биндингов к этому апи в GtkD нет, хотя оно достаточно простое, особенно в синхронном варианте.
Большинство проигрывателей поддерживает управление громкостью через стандарт ReplayGain и в GStreamer также есть плагины, например, rgvolume. Но отзывы об этом стандарте противоречивы, я не припомню его использования и не занимался добавлением.
А вот интеграцию в рабочий стол через MPRIS D-Bus Interface Specification добавить вполне можно, пример её работы можно увидеть, например, тут. Апи для работы с D-Bus есть в модуле gio, сложности могут возникнуть с построением типов шины через glib.Variant на основе сигнатур. Например, в словаре {sv} строковый тип ключа достаточно интуитивно и легко определить как new Variant("key"), а вот на "v" можно подумать как о сыром типе, однако это действительно new Variant(new Variant("value")).
XML-cпецификацию интерфейсов можно найти в репозиториях MPRIS и загрузить в gio.DBusNodeInfo, из которого теперь можно получить DBusInterfaceInfo, нужный для регистрации коллбэков в gio.DBusConnection.registerObject и registerObjectWithClosures, шина начнёт вызывать эти функции. В обратную сторону чуть сложнее через сигнал PropertiesChanged: хотя можно подсмотреть, как это делают другие плееры, но из-за небольшого различия в биндингах лучше всё-таки записать общение с шиной другого проигрывателя через тот же dbus-monitor и тщательно сверить с ним, например, у меня проскакивали лишние вложенные Variant в сравнении с VLC, из-за чего название трэка в плагине трея не менялось, но и ошибок никаких не было. Вообще, с этой шиной пришлось немного повозиться, мягко говоря.
Несложно заметить, что пайплайн для аудио очень близок по своей структуре к таковому для видео, тем более, что визуализация уже содержит в себе нужную логику, отсутствие поддержки видео выглядит обидным упущением. Самый простой вариант - перелинковать и перенаправить видео по ветке визуализации при переключении файлов в состоянии READY (пишут, что с паузой тоже работает), а потом вернуть всё на место снова. При таком подходе усложняется переключение между аудио\видео, ибо информацию нужно брать из самого файла (или адреса). В случае же динамической перестройки в состоянии PLAYING потребуются блокировки элементов и прочие сложности, что скорее и будет основным недостатком концепции цепочек, а значит и всей библиотеки. Но в любом случае перед линковкой на мультиплексор в addOnPadAdded есть смысл проверить Caps у Pad, чтобы случайно не пустить видео или аудио по неподходящему для них пути.
В моём случае нужно отлинковать визуализацию и направить видео по её пути, превращая таким образом плеер в телевизор:
Такой небольшой размер видео интереса не представляет, но поскольку кнопка для отображения визуализации в отдельном окне уже есть, то можно использовать её и для просмотра видео.
В целом, эксперимент можно признать удачным, хотя, конечно же, были определённые сложности соотнести С-api с биндингами, например, получение данных и структур из gstreamer.Message, но нужное сочетание можно вытыкать и простым перебором. Был ещё момент с неаккуратным вызовом unref при портировании сишного кода, что вело к коварным рандомным ошибкам, но диагностических сообщений вполне достаточно для их отлова. С другой стороны, любые проблемы компенсируются для меня удобством, скоростью и самодокументируемостью объектного апи и самого D, так что сам эксперимент понравился, а полученный опыт выглядит оправдывающим потраченное на это всё время.