Создаём GTK-видеоплеер с использованием Haskell

Создаём GTK-видеоплеер с использованием Haskell

Когда мы в последний раз остановились на Movie Monad, мы создали десктопный видео-плеер, использующий все веб-технологии (HTML, CSS, JavaScript и Electron). Фокус был в том, что весь исходный код проекта был написан на Haskell.

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

Мы могли бы продолжить развивать наш подход с вебом, настроив бэкенд на стриминг видеофайла в HTML5-сервер, запустив параллельно сервер и Electron-приложение. Вместо этого мы откажемся от веб-технологий и обратимся к GTK+, Gstreamer и системе управления окнами X11.

Если вы используете другую систему управления окнами, например, Wayland, Quartz или WinAPI, то этот подход может быть адаптирован для работы с вашим GDK-бэкендом. Адаптация заключается во встраивании выходного видеосигнала GStreamer playbin в окно Movie Monad.

GDK — важный аспект портируемости GTK+. Поскольку Glib уже предоставляет низкоуровневую кроссплатформенную функциональность, то чтобы заставить GTK+ работать на других платформах вам нужно только портировать GDK на базовый графический уровень операционной системы. То есть именно GDK-порты на Windows API и Quartz позволяют приложениям GTK+ исполняться на Windows и macOS (источник).

Для кого эта статья

  • Для программистов на Haskell, которые хотят реализовать пользовательский интерфейс на GTK+.
  • Для программистов, интересующихся функциональным программированием.
  • Для создателей GUI.
  • Для тех, кто ищет альтернативы GitHub Electron.
  • Для фанатов видео-плееров.

Что мы рассмотрим

  • Stack.
  • Привязки (bindings) haskell-gi
  • Директорию различных данных и файлы с ними.
  • Glade.
  • GTK+.
  • GStreamer.
  • Как создать Movie Monad.

Настройка проекта

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

Платформа Haskell

Если ваша машина ещё не готова к разработке Haskell-программ, то всё необходимое вы можете получить, скачав и установив платформу Haskell.

Stack

Если у вас ещё нет Stack, то обязательно установите его, прежде чем приступать к разработке. Но если вы уже пользовались платформой Haskell, то Stack у вас уже есть.

ExifTool

Прежде чем проигрывать видео в Movie Monad, нам нужно собрать кое-какую информацию о выбранном пользователем файле. Для этого воспользуемся ExifTool. Если вы работаете под Linux, то велик шанс, что у вас уже есть этот инструмент ( which exiftool ). ExifTool доступен для Windows, Mac и Linux.

Файлы проекта

Есть три способа получения файлов проекта.

Можете скачать ZIP-архив и извлечь их.

Можете сделать Git-клон с помощью SSH.

Можете склонировать git через HTTPS.

haskell-gi

haskell-gi умеет генерировать Haskell-привязки (bindings) к библиотекам, использующим связующее ПО для самодиагностики (introspection middleware) GObject. На момент написания статьи все необходимые привязки доступны на Hackage.

Зависимости

Теперь устанавливаем зависимости проекта.

Теперь настраиваем внедрение Movie Monad. Вы можете удалить исходные файлы и создать их заново, или следовать указаниям.

Paths_movie_monad.hs

Paths_movie_monad.hs используется для поиска файла Glade XML GUI во время runtime. Поскольку мы занимаемся разработкой, то будем использовать фиктивный модуль (dummy module) ( movie-monad/src/dev/Paths_movie_monad.hs ) для поиска файла movie-monad/src/data/gui.glade . После сборки/установки проекта реальный модуль Paths_movie_monad будет сгенерирован автоматически. Он предоставит нам функцию getDataFileName . Она присваивает своим выходным данным префикс в виде абсолютного пути, куда скопированы или установлены data-dir (movie-monad/src/) data-files .

Фиктивный модуль Paths_movie_monad .

Автоматически сгенерированный модуль Paths_movie_monad .

Main.hs

Main.hs — это входная точка для Movie Monad. В этом файле мы настраиваем наше окно с разными виджетами, подключаем GStreamer, а когда пользователь выходит, мы сносим окно.

Прагмы (Pragmas)

Нам нужно сказать компилятору (GHC), что нам нужны перегруженные (overloaded) строковые и лексически входящие в область видимости (lexically scoped) переменные типов.

OverloadedStrings позволяет нам использовать строковые литералы ( "Literal" ) там, где требуются String/[Char] или Text. ScopedTypeVariables позволяет нам использовать сигнатуру типа в паттерне параметра лямбда-функции, передаваемую для перехвата при вызове ExifTool.

Импорты

Поскольку мы работает с привязками Си, нам понадобится работать с типами, уже существующими в этом языке. Немалую часть импортов составляют привязки, генерируемые haskell-gi.

IsVideoOverlay

GStreamer-видеопривязки ( gi-gstvideo ) содержат класс типа (интерфейс) IsVideoOverlay . GStreamer-привязки ( gi-gst ) содержат тип элемента. Чтобы использовать элемент playbin с функцией GI.GstVideo.videoOverlaySetWindowHandle , нам нужно объявить GI.Gst.Element — экземпляр типа (type instance) IsVideoOverlay . А на стороне Cи playbin реализует интерфейс VideoOverlay .

Обратите внимание, что мы обёртываем GI.Gst.Element в новый тип (newtype), чтобы избежать появления потерянного (orphaned) экземпляра, поскольку мы объявляем экземпляр вне привязок haskell-gi.

Main — наша самая большая функция. В ней мы инициализируем все GUI-виджеты и определяем коллбэк-процедуры на основе определённых событий.

GI-инициализация

Здесь мы инициализировали GStreamer и GTK+.

Сборка GUI-виджетов

Как уже было сказано, мы получаем абсолютный путь к XML-файлу data/gui.glade , который описывает все наши GUI-виджеты. Дальше создаём из этого файла конструктор и получаем свои виджеты. Если бы мы не использовали Glade, то их пришлось бы создавать вручную, что довольно утомительно.

Playbin

Здесь мы создаём GStreamer-конвейер playbin . Он предназначен для решения самых разных нужд и экономит нам время на создании собственного конвейера. Назовём этот элемент MultimediaPlayer .

Встраиванние выходных данных GStreamer

Чтобы GTK+ и GStreamer заработали вместе, нам нужно сказать GStreamer, куда именно нужно выводить видео. Если этого не сделать, то GStreamer создаст собственное окно, поскольку мы используем playbin .

Здесь вы видите настройку коллбэка по мере готовности виджета drawingArea . Именно в этом виджете GStreamer должен показывать видео. Мы получаем родительское GDK-окно для виджета области отрисовки. Затем получаем обработчик окна, или XID системы X11 нашего окна GTK+. Строка CUIntPtr преобразует ID из CULong в CUIntPtr , необходимый для videoOverlaySetWindowHandle . Получив правильный тип, мы уведомляем GStreamer, что с помощью обработчика xid' он может отрисовывать в нашем окне выходные данные playbin .

Из-за бага в Glade мы программно скрываем полноэкранный виджет, поскольку если в Glade снять галочку visible box, то виджет всё-равно не будет спрятан.

Обратите внимание, что здесь нужно адаптировать Movie Monad для работы с оконной системой, если вы используете не Х-систему, а какую-то другую.

Выбор файла

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

  • Получаем имя файла из виджета выбора файла.
  • Говорим playbin , какой файл он должен воспроизвести.
  • Делаем уровень громкомсти в виджете таким же, как в playbin .
  • На основе желаемой ширины изображения и размера видео определяем подходящие ширину и высоту окна.
  • Если размеры окна успешно получены:
    • Начинаем воспроизведение файла.
    • Переводим кнопку пауза/воспроизведение в состояние ”on”.
    • Показываем полноэкранный виджет.
    • Если видео не в полноэкранном режиме:
    • Меняем размер окна, чтобы оно совпало с относительным размером видео.
    • Ставим playbin на паузу.
    • Переводим переключатель в положение ”off”.
    • Если это возможно, выводим окно из полноэкранного режима.
    • Сбрасываем размер окна.
    • Выводим маленькое диалоговое сообщение об ошибке.

    Пауза и воспроизведение

    Всё просто. Если переключатель в положении ”on”, то задаём элементу playbin состояние воспроизведения. В противном случае задаём ему состояние паузы.

    Настройка громкости

    При изменении уровня громкости в виджете мы передаём его значение в GStreamer, чтобы тот мог подстроить громкость воспроизведение.

    Перемещение по видео

    В Movie Monad есть шкала воспроизведения, в которой вы можете перемещать ползунок вперёд/назад, тем самым переходя по видеофреймам.

    Шкала от 0 до 100% представляет общую длительность видео-файла. Если переместить ползунок, например, на 50, то мы перейдём к временной отметке, находящийся посередине между началом и окончанием. Можно было бы настроить шкалу от нуля до значения длительности видео, но описанный метод более универсален.

    Обратите внимание, что для этого коллбэка мы используем сигнальный ID ( seekScaleHandlerId ), поскольку он понадобится нам позднее.

    Обновление шкалы воспроизведения

    Чтобы синхронизировать шкалу и сам процесс воспроизведения видео, нужно передавать сообщения между GTK+ и GStreamer. Каждую секунду мы будем запрашивать текущую позицию воспроизведения и в соответствии с ней обновлять шкалу. Так мы показываем пользователю, какая часть файла уже показана, а ползунок всегда будет соответствовать реальной позиции воспроизведения.

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

    Изменение размеров видео

    Этот виджет позволяет пользователю выбирать желаемую ширину видео. Высота будет подобрана автоматически на основе соотношения сторон видеофайла.

    Полноэкранный режим

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

    Обратите внимание, что мы не показываем виджет полноэкранного режима, если у нас нет видео.

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

    Благодаря использованию в главном потоке выполнения единственного пишущего и кучи сигнальных коллбэков, мы избегаем возможных ловушек общего изменяемого состояния. Если бы нас заботила безопасность потока выполнения, то вместо этого мы могли бы использовать MVar , TVar или atomicModifyIORef .

    О программе

    Последний рассматриваемый виджет — диалоговое окно «О программе». Здесь мы связываем диалоговое окно с кнопкой «О программе», отображающейся в основном окне.

    Закрытие окна

    Когда пользователь закрывает окно, мы уничтожаем конвейер playbin и выходим из основного цикла GTK.

    Запуск

    Наконец, мы показываем или отрисовываем главное окно и запускаем основной цикл GTK+. Он блокируется до вызова mainQuit .

    Полный файл Main.hs

    Ниже приведён файл movie-monad/src/Main.hs . Не показаны разные вспомогательные функции, относящиеся к main .

    Собираем Movie Monad

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

    Если всё в порядке, то Movie Monad должен запуститься.

    Заключение

    Пересмотрев проект Movie Monad, мы заново сделали приложение с помощью программных библиотек GTK+ и GStreamer. Благодаря им приложение осталось таким же портируемым, как и Electron-версия. Movie Monad теперь может обрабатывать большие видеофайлы и имеет все стандартные элементы управления.

    Другим преимуществом использования GTK+ стало уменьшение потребления памяти. Если сравнивать резидентный размер в памяти при старте, то версия GTK+ занимает

    50 Мб, а версия Electron —

    300 Мб (500%-ное увеличение).

    Наконец, вариант с GTK+ имеет меньше ограничений и требует меньше программирования. Для обеспечения такой же функциональности, вариант с Electron требует использования громоздкой клиент-серверной архитектуры. Но благодаря прекрасным сборкам haskell-gi мы смогли избежать решения на базе веба.

    Если хотите посмотреть другие приложения, построенные с помощью GTK+ и Haskell, то обратите внимание на Gifcurry. Оно умеет брать видеофайлы и на их основе создавать гифки с наложенным текстом.