В этом уроке мы рассмотрим исходный код утилиты, позволяющей проигрывать ролики с видеохостинга Vimeo. На предыдущем открытом уроке, состоявшемся в рамках онлайн-курса «Программист С» была создана программа, аналогичная известному опенсорсному продукту youtube-dl, который занимается скачиванием файлов с различных видеохостингов. Youtube-dl принимает на вход ссылку на страницу с видео и скачивает видеофайл для последующего локального просмотра любимым плеером. Мы используем часть кода с прошлого занятия, которая получает адрес видеофайла с конкретной страницы Vimeo. На этот раз мы увидим окно плеера с видео (а в идеале — и с аудио), и для этого мы будем использовать фреймворк GStreamer.

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

ООП на C: пишем видеоплеер

И то, и другое — опенсорсные библиотеки, у обоих достаточно пермиссивные лицензии, то есть их можно использовать в любых коммерческих приложениях. Безусловно, есть ряд других решений, в том числе библиотека libav, которая является форком FFMpeg, но две вышеперечисленных библиотеки можно назвать наиболее популярными. В чём же различия между ними?

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

Резюмируя, если нужно сделать что-то простое, FFMpeg подойдёт, но для более сложных задач имеет смысл обратить взгляд на фреймворк GStreamer. Это не просто библиотека, а целый программный каркас, на который можно насаживать свои элементы, которые занимаются обработкой видео, аудио и прочей мультимедийной информации. По своему внутреннему устройству GStreamer внешне похож на механизм фильтров DirectShow, с которым можно столкнуться при программировании под Windows.

ООП на C: пишем видеоплеер

DirectShow — это системная библиотека Windows для обработки видео. В частности, всякий раз, когда вы проигрываете видео на компьютере с данной ОС, под капотом у используемого вами видеоплеера создаётся так называемый pipeline (конвейер) из вышеупомянутых фильтров, и именно этот конвейер позволяет считывать, декодировать и отображать видео. GStreamer также предоставляет различные мультимедийные элементы и возможность комбинировать их в конвейер обработки.

Сам по себе GStreamer базируется на такой библиотеке, как GLib. Я люблю называть её «стандартной библиотекой на стероидах». Язык C разрабатывался в 70-х — 80-х годах прошлого века, и тогдашние взгляды на стандартную библиотеку языка были гораздо более минималистичными, буквально на уровне «можно открывать файлы — уже круто». Поэтому так сложилось, что в языке C стандартная библиотека, будем до конца честны, феноменально бедная, если сравнивать с более поздними языками — такими, как Python. В стандартной библиотеке последнего есть множество функций на любой случай жизни — можно скачать из интернета файл, закодировать данные в один из распространённых форматов вроде CSV или JSON, легко разобрать аргументы командной строки, и всё это — в стандартной поставке языка. В C же в подавляющем большинстве случаев придётся устанавливать сторонние библиотеки, чтобы сделать хоть что-либо.

Возвращаясь к GLib, это сторонняя библиотека, которая распространяется по пермиссивной лицензии LGPL. Она предоставляет множество различных подсистем, как то: отладочное журналирование, обработка ошибок, сетевые взаимодействия, контейнерные типы данных (хэш-таблица, красно-чёрное дерево и прочее, которых, конечно, нет в стандартной библиотеке C) и множество других.

Среди всего прочего, одна из подсистем GLib, называемая GObject, добавляет возможность использовать ООП в чистом C. Эта подсистема добавляет ООП-шные возможности не на уровне языка, а на уровне библиотеки, то есть для их использования всё-таки придётся написать некоторое количество бойлерплейта — вспомогательного кода, который не несёт никакой логики, но требуется для работы GObject. Для этого в библиотеке есть набор макросов, которые генерируют код для поддержки объектов. В GObject поддерживается даже наследование (кроме множественного).

Для более подробного ознакомления с ООП на основе GLib рекомендуется обратиться к статье «GObject: инкапсуляция, инстанциация, интроспекция», являющейся первой из цикла статей на Хабр, подробно описывающего простой пример с наследованием GObject’ов.

ООП в GLib строится на базе обычных C-шных структур, но в этих структурах также хранятся указатели на функции, которые используются в качестве виртуальных методов, то есть таких методов, которые могут быть переопределены в потомках класса при наследовании. Для каждого класса есть две структуры. Одна соответствует самому классу, и с ней происходит работа, когда речь идёт именно про класс всех возможных объектов и его поведение. Другая структура соответствует инстансу класса, то есть конкретному экземпляру. Для наследования используется интересный C-шный трюк. Рассмотрим его на примере из вышеупомянутой статьи:

struct _AnimalCatClass
{
    GObjectClass parent_class; /* родительская классовая структура */
    void (*say_meow) (AnimalCat*); /* виртуальный метод */
    gpointer padding[10]; /* массив указателей */
};

Мы определяем структуру, соответствующую нашему классу кошки _AnimalCatClass, и первым полем в ней мы определяем parent_class, имеющий тип GObjectClass, который также является структурой. Теперь за счёт этого поля и за счёт того, что оно идёт первым в определении нашей структуры, мы можем передавать её в те функции, которые ожидают в качестве параметра GObjectClass. Это работает благодаря расположению структуры в памяти: первые sizeof(GObjectClass) байт в нашей структуре полностью совпадают с GObjectClass, а всё, что идёт после них — это поля, специфичные для нашей структуры. В частности, padding — это резерв для последующих потомков нашей структуры, которые (возможно) будут переопределять виртуальные методы. Для того, чтобы их размер полностью совпадал с размером предка, у потомков размер поля padding нужно будет уменьшать за счёт добавления новых полей.

Далее мы определяем класс тигра _AnimalTiger:

struct _AnimalTiger
{
    AnimalCat parent; /* обязательно первым полем должен идти экземпляр родительского объекта */
    int speed; /* приватные данные */
};

И снова используем вышеупомянутый трюк: самым первым полем в структуре идёт структура AnimalCat, и за счёт этого в любую функцию, ожидающую кота, мы сможем передать тигра 🐯

Вернёмся к GStreamer. Когда мы проигрываем некий видео- или аудиофайл с помощью GStreamer, под капотом у приложения, чем бы оно ни было — видеоплеером, аудиоплеером или нашим C-шным приложением, каждый раз создаётся граф декодирования. Он может выглядеть, например, следующим образом:

ООП на C: пишем видеоплеер

Центральная часть фреймворка GStreamer, представляемая этим графом — так называемый pipeline, конвейер. Отдельными ступенями этого конвейера являются экземпляры класса GstElement, в терминологии фреймворка — элементы. Каждый элемент может быть одного из трёх типов.

  1. Это может быть элемент, порождающий медиаинформацию, такой элемент называется source, источник. В примере на картинке выше это file-source, считывающий информацию из файла на диске.
  2. Это может быть так называемый sink (дословно «сток»), являющийся конечной точкой конвейера. Он либо отображает пользователю медиа-информацию, либо утилизирует её другим образом — например, раздаёт по сети в качестве сервера. В примере на картинке у нас в конвейере сразу два стока (да, так тоже можно было), один — audio-sink, воспроизводящий аудио, другой — video-sink, отображающий видео.
  3. Могут быть промежуточные элементы, называемые фильтрами. В примере у нас три разных фильтра: ogg-demuxer, vorbis-decoder и theora-decoder. Суть фильтров в том, что они принимают на вход медиаинформацию, преобразуют её тем или иным образом и отдают дальше по конвейеру. Так, ogg-demuxer не занимается обработкой самой аудио или видеоинформации, он попросту разбирает формат контейнера OGG и достаёт из него эту информацию. В качестве других подобных примеров можно упомянуть другие demuxer’ы: для распространённого формата MP4, различных потоковых форматов, таких, как RTMP и RTSP, и так далее — все эти форматы поддерживаются GStreamer. Также в примере есть фильтры vorbis-decoder и theora-decoder, которые занимаются декодированием медиаинформации: в подавляющем большинстве случаев она хранится в сжатом виде, так как без сжатия даже небольшие по таймингу фрагменты могут занимать огромные объёмы памяти вплоть до десятков и сотен гигабайт. Фильтры-декодеры занимаются тем, что разжимают эту информацию для её дальнейшей обработки.

У каждого элемента есть способ соедиенения с другими элементами, называемый pads. Трудно подобрать адекватный перевод этого термина; отличительная особенность как GLib, так и GStreamer в том, что эти библиотеки широко используют локализацию и интернационализацию, то есть они по максимуму пытаются общаться с пользователем на его родном языке. Так вот, в отладочных журналах GStreamer при запуске программы с русскоязычной локалью можно встретить перевод термина pad как «контактное гнездо» — за неимением лучшего будем далее использовать этот вариант.

Изначально в конвейере элементы, как правило, не связаны друг с другом, если только программист не связал их эксплицитно в момент написания кода. Кроме того, каждый элемент, как правило, имеет различные контактные гнёзда, как входные, так и выходные, и каждый раз, когда GStreamer автоматически строит конвейер, он обнаруживает, каким оптимальным способом можно соединить друг с другом различные элементы. Этот процесс называется caps negotiation, что можно перевести как «переговоры о возможностях». Например, у ogg-demuxer есть два выходных контактных гнезда: для видео и для аудио, и мы не можем, например, взять его аудиовыход и соединить со входом декодера видео. Для работы механизма caps negotiation каждый элемент должен реализовать ряд виртуальных методов, вызываемых фреймворком во время построения конвейера для получения информации о контактных гнёздах элемента и поддерживаемых им типах контента, называемых caps (по-видимому, сокращение от capabilities, «возможности»), например, "video/x-h264" для видео, пожатого кодеком H.264. Формат, в котором задаются возможности, сильно похож на MIME-типы, которые можно часто встретить в web-программировании. Итак, GStreamer, в свою очередь, вызывает вышеуказанные методы и согласует между собой элементы в конвейере; если процесс согласования не удастся, фреймворк сообщит об ошибке.

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

Также в GLib существует механизм подсчёта ссылок, что несколько упрощает написание кода, в том числе с использованием GStreamer — не нужно беспокоиться о том, что буферы памяти, через которые передаётся медиаинформация в конвейере, не были удалены в нужный момент времени и таким образом создают утечку свободной памяти. По сути, GLib предоставляет некий рудиментарный lifetime management для GObject’ов, чуть более гибкий, чем C-шная модель с указателями и эксплицитным указанием всего и вся.

Хорошим стартом для нового проекта с использованием GStreamer будет заранее созданный авторами фреймворка бойлерплейт, упоминаемый в официальном руководстве. Есть два варианта получения этого бойлерплейта. Первый — склонировать себе готовый репозиторий:

git clone https://gitlab.freedesktop.org/gstreamer/gst-template.git

Этот вариант включает в себя довольно много кода, который можно счесть лишним — там есть C-шное приложение, использующее GStreamer, и шаблон простенького элемента.

Второй способ — использовать специальную утилиту gst-element-maker, входящую в пакет gst-plugins-bad. Однако у меня этот способ не заработал под несколькими разными дистрибутивами GNU/Linux — ни в одном из них мейнтейнеры не включили эту утилиту в готовый пакет, поэтому мне пришлось скомпилировать эту утилиту из исходников самому.

Итак, мы собираемся создать не полноразмерный плеер, а один небольшой source element конвейера GStreamer, который будет получать видео из ссылки на страницу на Vimeo и отдавать его для дальнейшей обработки.

Существуют два разных способа того, как наш source element будет отдавать свои данные в конвейер: pull-режим и push-режим, тяни или толкай 😃
В push-режиме источник сам генерирует события о том, что у него есть новые данные. В pull-режиме эти данные из него забираются явным образом по мере необходимости соединёнными с ним элементами. Pull модель лучше подходит для file-source, так как файл мы можем в произвольный момент времени проматывать и получать из него байты из произвольного места. Push-интерфейс больше подходит для источников, которые работают в live-режиме, например, элемент, который забирает аудиосигнал с микрофона, видео с вебкамеры, или сетевой поток по протоколу RTMP — во всех этих случаях мы не можем по запросу фреймворка «промотать» источник данных.

В нашем коде используется push-режим, так как он чуть проще в программировании. Можно было реализовать и в pull-режиме, добавить проматывание на произвольное место, синхронизацию для предотвращения «захлёбывания» и прочие необходимые для этого вещи, но код стал бы сложнее для восприятия.

Для источников, работающих в push-режиме, есть целый отдельный класс GstPushSrc, наследуемый (как и все прочие классы GStreamer) от GObject. Кроме того, в его цепочке наследования есть класс GstElement, представляющий собой элементы конвейера, и GstBaseSrc, являющийся базовым для всех элементов-источников. GstPushSrc — специальный класс источника, работающий только в push-режиме; в принципе, push-источник можно написать, отнаследовавшись от GstBaseSrc, но для этого понадобится чуть больше бойлерплейта.

Перейдём к коду нашего элемента; его можно найти в данном репозитории. Рассмотрим ключевые фрагменты различных файлов.

В корне репозитория есть несколько вспомогательных файлов, в том числе meson.build. Это файл с инструкциями для системы сборки Meson, написанной на Python и функционально похожую на более распространённую систему сборки CMake. Мы могли бы ограничиться классическим Makefile, но стартовый проект из официального руководства включает в себя сборку через Meson, а нам это только сыграет на руку, так как наш плагин будет зависеть от большого количества библиотек, и зависимости от этих библиотек будет проще прописать с помощью продвинутой системы сборки навроде Meson, нежели с помощью классической утилиты make.

Файл meson.build содержит на некотором псевдо-языке (DSL, Domain Specific Language) описание того, каким образом нужно собирать наш плагин. Meson принимает на вход это описание и генерирует входные файлы для более примитивной и низкоуровневой утилиты для сборки Ninja, которая и будет непосредственно заниматься сборкой. Ninja была разработана как альтернатива классическому make с повышенной скоростью работы.

Среди библиотек, от которых зависит наш плагин, понятное дело, будет GStreamer, последней версии 1.x:

gst_dep = dependency('gstreamer-1.0', version : '>=1.0',
    required : true, fallback : ['gstreamer', 'gst_dep'])

Также плагин будет зависеть от libcurl, поскольку как именно с помощью этой библиотеки мы будем качать из сети те байтики, которые представляют собой видео с Vimeo:

curl_dep = dependency('libcurl', version : '>= 7.66.0', required : true)
ООП на C: пишем видеоплеер

Мы используем libxml2 для парсинга той HTML-странички, которую нам будет отдавать Vimeo:

libxml2_dep = dependency('libxml2', version : '>= 2.9.3', required : true)
ООП на C: пишем видеоплеер

Кроме того, мы используем библиотеку Parson для разбора JSON, но эта библиотека подключена в виде своих исходников непосредственно в нашем репозитории, в каталоге contrib (от англ. contributed), через git submodule, поэтому мы попросту включаем единственный исходный файл этой библиотеки в список исходных файлов нашего плагина:

plugin_sources = [
  'src/vimeosource.c',
  'src/config.c',
  'src/http.c',
  'contrib/parson/parson.c'
  ]

И, наконец, финальным аккордом собираем всю информацию о сборке плагина в одном месте:

gstpluginexample = library('vimeosource',
  plugin_sources,
  c_args: plugin_c_args,
  dependencies : [gst_dep, gstvideo_dep, curl_dep, libxml2_dep],
  include_directories : include_directories('contrib/parson'),
  install : true,
  install_dir : plugins_install_dir,
)

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

plugin_path=$(realpath "$(dirname "$0")")/bin

Далее используется утилита, которая входит в базовую поставку самого GStreamer и называется gst-launch. Предназначение этой утилиты состоит в том, чтобы запускать произвольные контейнеры, переданные ей в текстовом виде:

gst-launch-1.0 -v -m --gst-plugin-path="$plugin_path" \
               vimeosource location=https://vimeo.com/59785024 ! decodebin name=dmux \
               dmux. ! queue ! audioconvert ! autoaudiosink \
               dmux. ! queue ! autovideoconvert ! autovideosink

В скрипте мы указываем каталог, в котором gst-launch надлежит искать дополнительные плагины, через аргумент командной строки --gst-plugin-path. Кроме того, мы включаем чуть более болтливые логи с помощью аргументов -v и -m. Затем мы передаём конвейер, который хотим запустить.

Первый элемент в конвейере — тот самый, который мы создаём; мы назвали его vimeosource. У данного элемента есть свойство location, в которое мы и передаём ссылку на страничку с видео. Восклицательный знак здесь — это аналог символа | в шелл-скриптах, он просто указывает, что мы соединяем два элемента друг с другом с помощью их контактных гнёзд. Далее в конвейере мы используем стандартный элемент decodebin, который умеет автоматически у себя под капотом создавать правильный элемент для демультиплексирования данных, которые в него приходят. По сути, decodebin — контейнерный элемент, его задача — содержать другие элементы и автоматически разбираться с тем, какого типа элементы требуется создавать. В нашем случае decodebin, скорее всего, создаст под капотом элемент h264parse, так как видео от Vimeo приходит в качестве H.264 потока.

Итак, мы будем передавать байты, полученные по сети, в элемент decodebin, который не занимается декодированием, а просто демультиплексирует входной поток данных. Впрочем, если бы мы соединили decodebin с элементом типа autovideosink, который воспроизводит видео в GUI-окне (то есть если бы мы пропустили промежуточные шаги), такой вариант по-прежнему работал бы, так как decodebin «понял» бы, что от него ожидают уже готовое к воспроизведению несжатое видео, и создал бы внутри себя дополнительные элементы для декодирования (скорее всего, avdec_h264).

В нашем случае мы поступаем чуть хитрее: задаём имя dmux элементу decodebin, и на этом данная часть конвейера заканчивается. Затем мы обращаемся к этому элементу по имени (dmux.) и соединяем его выход сразу с двумя элементами:

  1. Первое контактное гнездо соединяется с элементом queue, который занимается буферизацией, и соединяется затем с элементом audioconvert, который опять-таки автомагически декодирует аудио (учитывая специфику Vimeo, скорее всего с использованием кодека AAC) в обычный сырой звук, готовый для воспроизведения, которым, в свою очередь, занимается элемент autoaudiosink.
  2. Второе контактное гнездо decodebin тоже сначала соединяется с queue для буферизациии данных, затем с элементом autovideoconvert, который декодирует видео и передает готовое для воспроизведения видео в autovideosink.

Такой усложнённый конвейер с двумя элементами queue в нашем случае необходим для синхронизации аудиодорожки с видео.

Далее рассмотрим самый главный файл с исходным кодом src/vimeosource.c и соответствующий ему заголовочный файл src/vimeosource.h.

В src/vimeosource.h мы наследуемся от вышеупомянутого GstPushSrc. Структура, которая будет соответствовать объекту нашего элемента vimeosource_VimeoSource; каждый раз, когда в конвейере будет создаваться данный элемент, фреймворк будет создавать данную структуру.

struct _VimeoSource
{
    GstPushSrc base_vimeosource;
    gchar* location;
    gchar* file_location;

    CURLM* curlm;
    CURL* curl;
    GstBuffer* current_buffer;
};

В этой структуре у нас снова используется вышеописанный трюк: первым полем идёт структура GstPushSrc, а за ним — поля с некоторой информацией, которая необходима для работы нашего класса. На самом деле, если от класса планируется в дальнейшем наследоваться, то так делать не рекомендуется, вместо этого принято использовать специальный механизм под названием private data. Однако у нас дальнейшего наследования не предполагается, поэтому мы просто сваливаем все нужные поля в нашу структуру.

Далее мы определяем структуру, соответствующую классу — она будет создаваться лишь в единственном экземпляре на всё приложение:

struct _VimeoSourceClass
{
    GstPushSrcClass base_vimeosource_class;
};

В файле src/vimeosource.c располагается бизнес-логика. Первая функция, определяемая в файле — _vimeosource_class_init. Эта функция будет вызываться GLib единожды для инициализации класса нашего элемента. Тут можно усмотреть параллель с так называемыми метаклассами в языке Python, которые управляют созданием других классов. Объявление функции _vimeosource_class_init происходит автоматически с помощью макроса G_DEFINE_TYPE_WITH_CODE, который вызывается чуть выше:

G_DEFINE_TYPE_WITH_CODE(
    VimeoSource, _vimeosource, GST_TYPE_PUSH_SRC,
    GST_DEBUG_CATEGORY_INIT(_vimeosource_debug_category, "vimeosource", 0,
                            "debug category for vimeosource element"));

Данный макрос генерирует для нас довольно много кода, и, среди всего прочего, объявляет ряд функций, среди которых — функция инициализации класса.

Самое важное, что мы делаем в функции инициализации класса, — это установка в структуре класса указателей на функции, которые соответствуют переопределённым в нашем классе виртуальным методам:

static void _vimeosource_class_init(VimeoSourceClass* klass)
{
    GObjectClass* gobject_class = G_OBJECT_CLASS(klass);
    GstBaseSrcClass* base_src_class = GST_BASE_SRC_CLASS(klass);

    /* Код пропущен для ясности */

    gobject_class->set_property = _vimeosource_set_property;
    gobject_class->get_property = _vimeosource_get_property;
    gobject_class->finalize = _vimeosource_finalize;
    /* ... */

Прежде всего, мы переопределяем ряд виртуальных методов самого GObject, а именно — set_property, get_property и finalize. Последний занимается финализацией экземпляров класса, то есть примерно соответствует деструкторам C++, а set_property и get_property нам нужны для поддержки кастомного свойства location — того самого, которое мы задаём нашему элементу в конвейере. Механизм свойств не специфичен для GStreamer, он входит в GLib-овскую ООП-машинерию.

Далее мы переопределяем ряд виртуальных методов, определённых в предках нашего класса — GstElement, GstBaseSrc и GstPushSrc:

    base_src_class->negotiate = GST_DEBUG_FUNCPTR(_vimeosource_negotiate);
    base_src_class->start = GST_DEBUG_FUNCPTR(_vimeosource_start);
    base_src_class->stop = GST_DEBUG_FUNCPTR(_vimeosource_stop);
    base_src_class->query = GST_DEBUG_FUNCPTR(_vimeosource_query);
    base_src_class->create = GST_DEBUG_FUNCPTR(_vimeosource_create);

Метод negotiate является частью вышеописанного механизма caps negotiation — он сообщает фреймворку о том, подходят ли элементу заданные ему типы выходных данных. Наша реализация этого метода тривиальна — мы всегда возвращаем значение TRUE:

static gboolean _vimeosource_negotiate(GstBaseSrc* src)
{
    VimeoSource* vimeosource = _VIMEOSOURCE(src);

    GST_DEBUG_OBJECT(vimeosource, "negotiate");

    return TRUE;
}

то есть соглашаемся со всеми типами, которые от нас ожидает фреймворк (множество таких типов в любом случае ограничивается video/x-h264, указанным нами выше при создании контактного гнезда с помощью макроса GST_STATIC_PAD_TEMPLATE). Без переопределения этого метода, к сожалению, элемент не заработает, так как caps negotiation для него будет завершаться неудачей.

Метод query вызывается фреймворком для запрашивания некоторой мета-информации о текущем состоянии нашего элемента. В его реализации мы сначала делаем отладочный вывод — добавляем в журнал сообщение о том, что этот метод был вызван с определённым запросом:

static gboolean _vimeosource_query(GstBaseSrc* src, GstQuery* query)
{
    VimeoSource* vimeosource = _VIMEOSOURCE(src);

    GST_DEBUG_OBJECT(vimeosource, "query %s",
                     gst_query_type_get_name(GST_QUERY_TYPE(query)));

Далее, по сути, все запросы о состоянии элемента мы перенаправляем родительским классам:

    if(!ret)
        ret = GST_BASE_SRC_CLASS(_vimeosource_parent_class)->query(src, query);

    return ret;

Макрос GST_BASE_SRC_CLASS возвращает базовый класс, мы обращаемся через -> к полю его структуры, которое будет указателем на функцию-метод, и, наконец, вызываем эту функцию с теми аргументами, которые мы получили от фреймворка.

Однако запрос типа GST_QUERY_URI мы обрабатываем самостоятельно:

    switch(GST_QUERY_TYPE(query))
    {
    case GST_QUERY_URI:
        gst_query_set_uri(query, vimeosource->location);
        ret = TRUE;
        break;

В ответ на такой запрос мы возвращаем текущий location, выставленный для нашего элемента. Здесь vimeosource — это экземпляр структуры _VimeoSource, и для получения необходимой информации мы попросту обращаемся к её полю location. Затем мы сохраняем полученную строку с помощью вызова функции gst_query_set_uri в полученный аргументом объект типа GstQuery, который является многофункциональным объектом, способным также хранить произвольную информацию, полученную в качестве ответа на запрос от фреймворка.

Мы могли бы и не переопределять метод query, но всё-таки делаем это из следующих соображений. Дело в том, что в GStreamer есть элементы, аналогичные вышерассмотренному decodebin, которые автоматически создают дочерние элементы, и подобный запрос URL (точнее, URI — Universal Resource Identifier) может быть отправлен такими элементами для определения того, какого типа дочерний элемент должен быть создан для заданного этим элементам URI. Мы не планируем пользоваться этим механизмом, но для порядка реализуем 👌

Методы start и stop отвечают за начало и конец работы нашего элемента в рамках конвейра. В start мы должны инициализировать ресурсы, необходимые для работы, а в stop — финализировать их. В нашей реализации метода start довольно много кода, но он достаточно простой, как и практически любой код на C 😊

Прежде всего, мы записываем в журнал тот факт, что метод был вызван:

static gboolean _vimeosource_start(GstBaseSrc* src)
{
    VimeoSource* vimeosource = _VIMEOSOURCE(src);

    GST_DEBUG_OBJECT(vimeosource, "start");

Затем получаем непосредственный URL видеофайла по URL страницы на Vimeo, который нам передали в свойстве location. Для этого мы используем код из предыдущего открытого урока, а именно — функцию get_file_url, объявленную в src/config.h и определённую в src/config.c. Функция работает с libcurl для загрузки страницы видео, ищет на странице JSON-объект, в котором прописан специальный config URL, затем обращается на этот URL, получает в ответ ещё порцию JSON’а, и уже из него получает ссылки на медиафайлы. После чего функция запускает цикл, который проходится по всем медиафайлам и ищет тот, у которого наибольшее разрешение, а найдя, возвращает его. В предыдущем открытом уроке мы подробнее рассматривали все эти механизмы.

Мы записываем полученный URL видеофайла в поле file_location нашей структуры VimeoSource:

    setlocale(LC_NUMERIC, "C"); // see https://git.io/Jte2C
    vimeosource->file_location = get_file_url(vimeosource->location);
    setlocale(LC_NUMERIC, "");

Здесь сразу виден большой подводный камень, про который я сам знал, но забыл и при подготовке этого кода угробил почти целый день, пытаясь понять, что же пошло не так. Суть проблемы в том, что библиотека для разбора JSON Parson разбирает числовые поля с помощью функции strtod, которая конвертирует строки в числа с плавающей запятой, но эта функция зависит от текущей локали. В русскоязычной локали разделителем десятичных разрядов является запятая, а не точка, как это принято в стандартной английской локали. Таким образом, при запуске кода с русскоязычной локалью запятая, которая идёт после поля, считается частью числа, и поэтому парсинг заканчивается неудачей. Что интересно, автору библиотеки неоднократно писали про эту проблему в багтрекер, но он эту особенность исправлять отказался, мол, локали не во власти моей библиотеки. Чтобы исправить эту проблему, на время получения URL на видеофайл через get_file_url, у которой под капотом и происходит разбор JSON через Parson, мы временно переключаем аспект LC_NUMERIC локали, отвечающий за форматирование чисел, на стандартную, так называемую C locale.

Далее мы журналируем полученный на предыдущем шаге URL видеофайла:

    GST_DEBUG_OBJECT(vimeosource, "location=%s", vimeosource->file_location);

Затем мы производим манипуляции, связанные с инициализацией libcurl:

    vimeosource->curlm = curl_multi_init();
    g_assert(vimeosource->curlm);
    if(!vimeosource->curlm)
        return GST_FLOW_ERROR;

В libcurl, среди всего прочего, есть механизм под названием multi interface, который позволяет скомбинировать несколько закачек в одну в асинхронном режиме; предполагается, что он будет использоваться вместе с каким-либо механизмом мультиплескирования ввода-вывода вроде select или poll, либо со своей родной функцией для ожидания curl_multi_poll. Мы будем использовать этот механизм по следующим соображениям. Каждый раз, когда фреймворк будет вызывать метод create у нашего элемента, он должен будет возвращать очередной буфер с накопившимися на данный момент данными, такова суть работы источника в push-режиме. Если бы мы использовали простой curl easy interface, у нас бы не получилось выстроить такую схему, т.к. при вызове curl_easy_perform управление не возвращается из этой функции до тех пор, пока файл не будет скачан до конца; нас это не удовлетворяет, т.к. нам нужно скачать файл не за раз целиком, а скармливать фреймворку маленькими кусочками.

Мы создаём обычный curl easy handle, устанавливаем для него все нужные нам параметры с помощью функции curl_easy_setopt и добавляем его в multi handle через функцию curl_multi_add_handle:

    vimeosource->curl = curl_easy_init();
    g_assert(vimeosource->curl);
    if(!vimeosource->curl)
        return GST_FLOW_ERROR;

    curl_easy_setopt(vimeosource->curl, CURLOPT_URL,
                     vimeosource->file_location);
    curl_easy_setopt(vimeosource->curl, CURLOPT_USERAGENT, useragent);
    curl_easy_setopt(vimeosource->curl, CURLOPT_WRITEDATA, src);
    curl_easy_setopt(vimeosource->curl, CURLOPT_WRITEFUNCTION, &curl_callback);

    ret = curl_multi_add_handle(vimeosource->curlm, vimeosource->curl);
    g_assert(ret == CURLM_OK);
    if(ret != CURLM_OK)
        return GST_FLOW_ERROR;

В качестве callback’а (функции обратного вызова) для easy handle мы выставляем нашу функцию curl_callback, которая по сути создаёт те буферы с данными, которые от нас ожидает фреймворк, и возвращает их. Через аргумент callback’а WRITEDATA мы передаём сам экземпляр нашего класса VimeoSource. В нём у нас есть поле current_buffer, в которое в callback’е мы будем прост класть очередной буфер, который удалось считать из сети.

Далее мы вызываем функцию curl_multi_perform, которая не блокирует управление, а возвращает его после начала работы переданного ей multi handle:

    int dummy;
    ret = curl_multi_perform(vimeosource->curlm, &dummy);
    g_assert(ret == CURLM_OK);
    if(ret != CURLM_OK)
        return GST_FLOW_ERROR;

    return TRUE;
}

Метод stop — злой брат-близнец метода start, он просто уничтожает в обратном созданию порядке все ресурсы, инициализированные в start. Мы уничтожаем созданный multi handle, если он есть:

static gboolean _vimeosource_stop(GstBaseSrc* src)
{
    VimeoSource* vimeosource = _VIMEOSOURCE(src);
    if(vimeosource->curlm)
    {
        CURLMcode ret
            = curl_multi_remove_handle(vimeosource->curlm, vimeosource->curl);
        g_assert(ret == CURLM_OK);
        if(ret != CURLM_OK)
            return FALSE;

        ret = curl_multi_cleanup(vimeosource->curlm);
        g_assert(ret == CURLM_OK);
        if(ret != CURLM_OK)
            return FALSE;

        vimeosource->curlm = NULL;
    }

так же поступаем с easy handle:

    if(vimeosource->curl)
    {
        curl_easy_cleanup(vimeosource->curl);
        vimeosource->curl = NULL;
    }

и освобождаем память, отведённую под строки, хранившие URL страницы на Vimeo и URL видеофайла:

    g_free(vimeosource->file_location);
    vimeosource->file_location = NULL;

    g_free(vimeosource->location);
    vimeosource->location = NULL;

    return TRUE;
}

Наконец, сердце нашего элемента — метод create. Вопреки названию, он не связан с созданием элемента; напротив, он вызывается фреймворком в тот момент, когда GStreamer от нас нужен очередной буфер с данными. Под названием create подразумевается, что мы создаём этот самый буфер. GStreamer передаёт нам аргументом двойной указатель на этот буфер buf, изначально указывающий на нулевой указатель, и мы по этому указателю должны положить вместо NULL новосозданный буфер, содержащий очередной фрагмент данных. Мы делаем это под конец функции — просто кладём туда то самое поле current_buffer из структуры нашего элемента и возвращаем значение GST_FLOW_OK, сигнализирующее о том, что мы удачно создали буфер:

static GstFlowReturn _vimeosource_create(GstBaseSrc* src, guint64 offset,
                                         guint size, GstBuffer** buf)
{
    VimeoSource* vimeosource = _VIMEOSOURCE(src);

    /* ... */

    *buf = vimeosource->current_buffer;
    return GST_FLOW_OK;
}

Вся соль нашей реализации в том, что мы в цикле вызываем функцию curl_multi_poll, которая похожа на обычный системный вызов poll — она постоянно опрашивает (англ. to poll) дескрипторы, которые есть под капотом у переданного ей curl multi handle до тех пор, пока на каком-то из этих дескрипторов не появятся новые данные. Как только данные появляются, мы на этой же итерации цикла вызываем функцию curl_multi_perform. Последняя функция довольно хитрая: она может вызвать наш callback с прочитанными данными, а может в ряде случаев (например, при обработке HTTP-заголовков) и не вызвать, и при этом вернуть значение CURLM_OK, сигнализирующее о том, что всё прошло успешно. Именно поэтому мы в цикле while, пока не получим из callback’а новый буфер, вызываем эти функции: сначала с помощью curl_mutli_poll ждём появления новых данных, затем через curl_mutli_perform обрабатываем эти новые данные с помощью callback’а, и, когда в результате обработки мы получаем непустой current_buffer, мы его и возвращаем:

    vimeosource->current_buffer = NULL;

    while(!vimeosource->current_buffer)
    {
        gint numfds = 0;
        ret = curl_multi_poll(vimeosource->curlm, NULL, 0, 0, &numfds);
        g_assert(ret == CURLM_OK);
        if(ret != CURLM_OK)
            return GST_FLOW_ERROR;

        gint running;
        ret = curl_multi_perform(vimeosource->curlm, &running);
        g_assert(ret == CURLM_OK);
        if(ret != CURLM_OK)
            return GST_FLOW_ERROR;
        if(!running)
            break;
    }

    *buf = vimeosource->current_buffer;
    return GST_FLOW_OK;
}

Теперь выходит на сцену вышеупомянутый механизм подсчёта ссылок из GLib. В callback’е мы выделяем кусок памяти под сами данные:

static size_t curl_callback(void* contents, size_t size, size_t nmemb,
                            void* userp)
{
    GstBaseSrc* src = userp;
    VimeoSource* vimeosource = _VIMEOSOURCE(src);
    gsize realsize = size * nmemb;

    vimeosource->current_buffer = gst_buffer_new();

    gchar* data = g_malloc(realsize);
    g_assert(data);
    if(!data)
        return 0;
    memcpy(data, contents, realsize);

затем создаём регион памяти GstMemory, являющийся тонкой обёрткой над этим куском памяти, через функцию gst_memory_new_wrapped; последним аргументом эта функция принимает указатель на функцию, которая будет вызвана для удаления этого региона памяти: мы попросту передаём в качестве такой функции g_free, которая соответствует предыдущему вызову g_malloc:

    GstMemory* memory
        = gst_memory_new_wrapped(0, data, realsize, 0, realsize, data, g_free);
    g_assert(memory);
    if(!memory)
        return 0;

Далее, в начале функции мы создаём новый буфер в current_buffer через вызов gst_buffer_new, а под конец добавляем в буфер наш регион памяти:

    vimeosource->current_buffer = gst_buffer_new();

    /* ... */

    gst_buffer_insert_memory(vimeosource->current_buffer, -1, memory);

    return realsize;
}

Возвращаясь к подсчёту ссылок, мы создали буфер, динамический объект, через функцию gst_buffer_new, но при этом мы нигде в нашем коде не вызываем функцию для его удаления. Несмотря на это, у нас не будет происходить утечка памяти (я проверял 😂). Не происходит она потому, что буфер создаётся со счётчиком ссылок, равным 1. Мы его отправляем во фреймворк через аргумент buf в методе create, фреймворк этот буфер нужным ему образом обрабатывает, и, когда буфер становится ему ненужным, уменьшает счётчик ссылок на 1, в результате чего он становится равным нулю, и внутренняя машинерия GLib автоматически удаляет этот динамический объект.

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

Наконец, для того, чтобы запустить наш код, нужно в корневом каталоге проекта создать каталог с именем bin, затем в этом каталоге вызвать команду meson .., чтобы Meson нам сгенерировал файлы для сборки через Ninja (а именно, файл build.ninja):

$ mkdir -p bin
$ cd bin
$ meson ..
The Meson build system
Version: 0.43.0
Source dir: /home/andrew/Progs/otus-video-player
Build dir: /home/andrew/Progs/otus-video-player/bin
Build type: native build
Project name: otus-video-player
Native C compiler: cc (gcc 5.4.0)
Build machine cpu family: x86_64
Build machine cpu: x86_64
Found pkg-config: /usr/bin/pkg-config (0.29.1)
Native dependency gstreamer-1.0 found: YES 1.8.3
Native dependency libcurl found: YES 7.76.1
Native dependency libxml2 found: YES 2.9.3
Configuring config.h using configuration
Native dependency gstreamer-video-1.0 found: YES 1.8.3
Build targets in project: 1
Found ninja-1.9.0 at /usr/bin/ninja

При этом Meson сообщит нам инфорацию о собственной версии, версии компилятора и о версиях запрошенных нами через файл meson.build библиотек. Далее, для непосредственной сборки нашего плагина, мы просто без аргументов вызываем ninja.
Результат сборки — файл libvimeosource.so, это разделяемая библиотека (SO — shared object). Мы можем заглянуть ей под капот и посмотреть на функции, которые экспортируются и импортируются в неё, с помощью команды nm -D libvimeosource.so:

$ nm -D libvimeosource.so
000000000020f4d8 B __bss_start
                 U __ctype_b_loc
                 U curl_easy_cleanup
                 U curl_easy_init
                 U curl_easy_perform
                 U curl_easy_setopt
                 U curl_global_cleanup
                 U curl_global_init
                 U curl_multi_add_handle
                 U curl_multi_cleanup
                 U curl_multi_init
                 U curl_multi_perform
                 U curl_multi_poll
                 U curl_multi_remove_handle
                 w __cxa_finalize
0000000000005b7b T do_request
000000000020f4d8 D _edata
000000000020f500 B _end
                 U __errno_location
                 U fclose
                 U ferror
000000000000bcdc T _fini
                 U fopen64
                 U fputs
                 U fread
                 U free
                 U fseek
                 U ftell
                 U g_assertion_message_expr
00000000000053ab T get_config_url
00000000000056ac T get_file_url
                 U g_free
                 U g_intern_static_string
                 U g_log
                 U g_malloc
                 w __gmon_start__
                 U g_object_class_install_property
                 U g_once_init_enter
                 U g_once_init_leave
                 U g_param_spec_string
                 U g_realloc
                 U gst_base_src_get_type
                 U gst_buffer_insert_memory
                 U gst_buffer_new
                 U _gst_debug_category_new
                 U gst_debug_log
                 U _gst_debug_min
                 U _gst_debug_register_funcptr
                 U gst_element_class_add_static_pad_template
                 U gst_element_class_set_static_metadata
                 U gst_element_get_type
                 U gst_element_register
                 U gst_memory_new_wrapped
000000000020f440 D gst_plugin_desc
                 U gst_push_src_get_type
                 U gst_query_set_uri
                 U gst_query_type_get_name
                 U g_strdup
                 U g_strndup
                 U g_type_check_class_cast
                 U g_type_check_instance_cast
                 U g_type_class_adjust_private_offset
                 U g_type_class_peek_parent
                 U g_type_name
                 U g_type_register_static_simple
                 U g_value_get_string
                 U g_value_set_string
                 U htmlCreateMemoryParserCtxt
                 U htmlCtxtUseOptions
                 U htmlParseDocument
00000000000037d0 T _init
                 w _ITM_deregisterTMCloneTable
                 w _ITM_registerTMCloneTable
000000000000bc11 T json_array
000000000000ad9d T json_array_append_boolean
000000000000adfa T json_array_append_null
000000000000ad36 T json_array_append_number
000000000000ac6b T json_array_append_string
000000000000accb T json_array_append_string_with_len
000000000000ac25 T json_array_append_value
000000000000abb4 T json_array_clear
0000000000009ae7 T json_array_get_array
0000000000009b14 T json_array_get_boolean
0000000000009b41 T json_array_get_count
0000000000009a7f T json_array_get_number
0000000000009aba T json_array_get_object
0000000000009a25 T json_array_get_string
0000000000009a52 T json_array_get_string_len
00000000000099dd T json_array_get_value
0000000000009b61 T json_array_get_wrapping_value
000000000000a847 T json_array_remove
000000000000aaf2 T json_array_replace_boolean
000000000000ab57 T json_array_replace_null
000000000000aa83 T json_array_replace_number
000000000000a9a8 T json_array_replace_string
000000000000aa10 T json_array_replace_string_with_len
000000000000a90f T json_array_replace_value
000000000000bc87 T json_boolean
000000000000a828 T json_free_serialized_string
000000000000bc5f T json_number
000000000000bbf7 T json_object
000000000000b544 T json_object_clear
00000000000097c6 T json_object_dotget_array
00000000000097f3 T json_object_dotget_boolean
000000000000975e T json_object_dotget_number
0000000000009799 T json_object_dotget_object
0000000000009704 T json_object_dotget_string
0000000000009731 T json_object_dotget_string_len
000000000000967a T json_object_dotget_value
000000000000995f T json_object_dothas_value
000000000000998d T json_object_dothas_value_of_type
000000000000b51a T json_object_dotremove
000000000000b42e T json_object_dotset_boolean
000000000000b493 T json_object_dotset_null
000000000000b3bf T json_object_dotset_number
000000000000b2e4 T json_object_dotset_string
000000000000b34c T json_object_dotset_string_with_len
000000000000b10b T json_object_dotset_value
0000000000009620 T json_object_get_array
000000000000964d T json_object_get_boolean
0000000000009820 T json_object_get_count
0000000000009840 T json_object_get_name
00000000000095b8 T json_object_get_number
00000000000095f3 T json_object_get_object
000000000000955e T json_object_get_string
000000000000958b T json_object_get_string_len
0000000000009515 T json_object_get_value
0000000000009888 T json_object_get_value_at
00000000000098d0 T json_object_get_wrapping_value
00000000000098e1 T json_object_has_value
000000000000990f T json_object_has_value_of_type
000000000000b4f0 T json_object_remove
000000000000b06f T json_object_set_boolean
000000000000b0c1 T json_object_set_null
000000000000b013 T json_object_set_number
000000000000af5e T json_object_set_string
000000000000afb3 T json_object_set_string_with_len
000000000000ae4f T json_object_set_value
0000000000009335 T json_parse_file
000000000000938d T json_parse_file_with_comments
00000000000093e5 T json_parse_string
0000000000009449 T json_parse_string_with_comments
000000000000a3c8 T json_serialization_size
000000000000a5f8 T json_serialization_size_pretty
000000000000a434 T json_serialize_to_buffer
000000000000a664 T json_serialize_to_buffer_pretty
000000000000a4ae T json_serialize_to_file
000000000000a6de T json_serialize_to_file_pretty
000000000000a564 T json_serialize_to_string
000000000000a794 T json_serialize_to_string_pretty
000000000000bca1 T json_set_allocation_functions
000000000000bcc6 T json_set_escape_slashes
000000000000bc2b T json_string
000000000000bc45 T json_string_len
000000000000bbdd T json_type
000000000000b5da T json_validate
000000000000a077 T json_value_deep_copy
000000000000b877 T json_value_equals
0000000000009cfc T json_value_free
0000000000009bbf T json_value_get_array
0000000000009cb0 T json_value_get_boolean
0000000000009c82 T json_value_get_number
0000000000009b91 T json_value_get_object
0000000000009cdd T json_value_get_parent
0000000000009c1b T json_value_get_string
0000000000009c4e T json_value_get_string_len
0000000000009b72 T json_value_get_type
0000000000009df0 T json_value_init_array
0000000000009fdb T json_value_init_boolean
000000000000a033 T json_value_init_null
0000000000009f46 T json_value_init_number
0000000000009d71 T json_value_init_object
0000000000009e6f T json_value_init_string
0000000000009ea9 T json_value_init_string_with_len
                 w _Jv_RegisterClasses
                 U malloc
                 U memcmp
                 U memcpy
                 U memmove
                 U rewind
                 U setlocale
                 U sprintf
                 U __stack_chk_fail
                 U strchr
                 U strcmp
                 U strlen
                 U strncmp
                 U strncpy
                 U strstr
                 U strtod
000000000020f4b8 D useragent
00000000000041cd T _vimeosource_get_type
                 U xmlStrlen
                 U xmlStrstr

Мы можем увидеть внутренние функции для разбора JSON, которые мы использовали — у них префикс json_, а также импорты стандартных библиотечных функций, как то: malloc, memcmp и так далее, а также импорты функций из библиотеки libcurl и из самого GStreamer.

Так как это библиотека, запустить напрямую мы её не можем, но зато у нас есть скрипт для запуска launch.sh, который умеет её подгружать в конвейер GStreamer. Мы можем убедиться, что GStreamer подргужает именно наш код следующим образом:

$ rm libvimeosource.so
$ ../launch.sh
ПРЕДУПРЕЖДЕНИЕ: ошибочный конвейер: элемент «vimeosource» не найден

Почему элемент не найден? Да потому что я его удалил.

Скомпилируем библиотеку заново через вызов ninja и запустим ../launch.sh. В качестве примера видео для воспроизведения в скрипте прописана ссылка на мультфильм под названием Sintel, и мы можем убедиться, что он действительно воспроизводится и даже проигрывает звук 🎉

ООП на C: пишем видеоплеер

Sintel — коротенький мультфильм с душераздирающим сюжетом, созданный для рекламы опенсорсного пакета 3D-моделирования Blender, который, кстати, тоже написан на чистом C.

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

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