В этом уроке мы рассмотрим исходный код утилиты, позволяющей проигрывать ролики с видеохостинга Vimeo. На предыдущем открытом уроке, состоявшемся в рамках онлайн-курса «Программист С» была создана программа, аналогичная известному опенсорсному продукту youtube-dl, который занимается скачиванием файлов с различных видеохостингов. Youtube-dl принимает на вход ссылку на страницу с видео и скачивает видеофайл для последующего локального просмотра любимым плеером. Мы используем часть кода с прошлого занятия, которая получает адрес видеофайла с конкретной страницы Vimeo. На этот раз мы увидим окно плеера с видео (а в идеале — и с аудио), и для этого мы будем использовать фреймворк GStreamer.
В случаях, когда в программе нужна обработка мультимедийных данных, будь то видео или аудио, в независимости от используемого языка (C, C++ или что-то другое) есть два наиболее распространённых варианта для добавления такой функциональности: GStreamer и FFMpeg.
И то, и другое — опенсорсные библиотеки, у обоих достаточно пермиссивные лицензии, то есть их можно использовать в любых коммерческих приложениях. Безусловно, есть ряд других решений, в том числе библиотека libav, которая является форком FFMpeg, но две вышеперечисленных библиотеки можно назвать наиболее популярными. В чём же различия между ними?
FFMpeg — сравнительно простая библиотека, достаточно использовать несколько её функций со стабильным API для того, чтобы, например, проиграть видеофайл, или последовательно получать из него кадр за кадром и работать с каждым из них как с отдельным изображением; для этого достаточно написать кусок кода длиной в полторы сотни строчек. Проблема в том, что как только возникает нужда решить более сложную задачу, произвести некий нетривиальный анализ, или некоторое перекодирование, то всё становится намного сложнее — приходится лезть в исходники самой библиотеки и вызывать не экспортируемые наружу API, которые при следующих релизах библиотеки меняются или пропадают вовсе, и, как следствие, программа работает только с одной конкретной версией FFMpeg.
Резюмируя, если нужно сделать что-то простое, FFMpeg подойдёт, но для более сложных задач имеет смысл обратить взгляд на фреймворк GStreamer. Это не просто библиотека, а целый программный каркас, на который можно насаживать свои элементы, которые занимаются обработкой видео, аудио и прочей мультимедийной информации. По своему внутреннему устройству GStreamer внешне похож на механизм фильтров DirectShow, с которым можно столкнуться при программировании под Windows.
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-шным приложением, каждый раз создаётся граф декодирования. Он может выглядеть, например, следующим образом:
Центральная часть фреймворка GStreamer, представляемая этим графом — так называемый pipeline, конвейер. Отдельными ступенями этого конвейера являются экземпляры класса GstElement, в терминологии фреймворка — элементы. Каждый элемент может быть одного из трёх типов.
- Это может быть элемент, порождающий медиаинформацию, такой элемент называется source, источник. В примере на картинке выше это file-source, считывающий информацию из файла на диске.
- Это может быть так называемый sink (дословно «сток»), являющийся конечной точкой конвейера. Он либо отображает пользователю медиа-информацию, либо утилизирует её другим образом — например, раздаёт по сети в качестве сервера. В примере на картинке у нас в конвейере сразу два стока (да, так тоже можно было), один — audio-sink, воспроизводящий аудио, другой — video-sink, отображающий видео.
- Могут быть промежуточные элементы, называемые фильтрами. В примере у нас три разных фильтра: 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)
Мы используем libxml2 для парсинга той HTML-странички, которую нам будет отдавать Vimeo:
libxml2_dep = dependency('libxml2', version : '>= 2.9.3', required : true)
Кроме того, мы используем библиотеку 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.) и соединяем его выход сразу с двумя элементами:
- Первое контактное гнездо соединяется с элементом queue, который занимается буферизацией, и соединяется затем с элементом audioconvert, который опять-таки автомагически декодирует аудио (учитывая специфику Vimeo, скорее всего с использованием кодека AAC) в обычный сырой звук, готовый для воспроизведения, которым, в свою очередь, занимается элемент autoaudiosink.
- Второе контактное гнездо 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, и мы можем убедиться, что он действительно воспроизводится и даже проигрывает звук 🎉
Sintel — коротенький мультфильм с душераздирающим сюжетом, созданный для рекламы опенсорсного пакета 3D-моделирования Blender, который, кстати, тоже написан на чистом C.
Подводя итог, мы написали библиотеку, которая содержит внутри себя элемент конвейра GStreamer, и мы успешно использовали этот элемент для воспроизведения видео с Vimeo.
Важная особенность состоит в том, что наша библиотека универсальна, мы можем использовать написанный нами элемент в произвольных конвейерах, в том числе теоретически можно взять произвольное приложение, использующее GStreamer, например, какой-нибудь третьесторонний плеер, и заставить его использовать наш элемент и воспроизводить видео с Vimeo. Таким образом, мы на конкретном примере наблюдаем гибкость данного фреймворка.
<!DOCTYPE html>
<html dir="ltr" lang="ru-RU">
<head>
<meta charset="UTF-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="profile" href="http://gmpg.org/xfn/11" />
<title>ООП на C: пишем видеоплеер OTUS</title>
<!-- All in One SEO 4.5.2.1 - aioseo.com -->
<meta name="description" content="В этом уроке мы рассмотрим исходный код утилиты, позволяющей проигрывать ролики с видеохостинга Vimeo. На предыдущем открытом уроке, состоявшемся в рамках онлайн-курса «Программист С» была создана программа, аналогичная известному опенсорсному продукту youtube-dl, который занимается скачиванием файлов с различных видеохостингов. Youtube-dl принимает на вход ссылку на страницу с видео и скачивает видеофайл для последующего локального просмотра" />
<meta name="robots" content="max-image-preview:large" />
<link rel="canonical" href="https://otus.ru/journal/oop-na-c-pishem-videopleer/" />
<meta name="generator" content="All in One SEO (AIOSEO) 4.5.2.1" />
<script type="application/ld+json" class="aioseo-schema">
{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/otus.ru\/journal\/oop-na-c-pishem-videopleer\/#article","name":"\u041e\u041e\u041f \u043d\u0430 C: \u043f\u0438\u0448\u0435\u043c \u0432\u0438\u0434\u0435\u043e\u043f\u043b\u0435\u0435\u0440 OTUS","headline":"\u041e\u041e\u041f \u043d\u0430 C: \u043f\u0438\u0448\u0435\u043c \u0432\u0438\u0434\u0435\u043e\u043f\u043b\u0435\u0435\u0440","author":{"@id":"https:\/\/otus.ru\/journal\/author\/k-moseenkova\/#author"},"publisher":{"@id":"https:\/\/otus.ru\/journal\/#organization"},"image":{"@type":"ImageObject","url":"https:\/\/otus.ru\/journal\/wp-content\/uploads\/2021\/05\/oj-1080x720-kopiya-1-1.png","width":1080,"height":720},"datePublished":"2021-05-24T14:51:07+00:00","dateModified":"2021-05-24T17:34:06+00:00","inLanguage":"ru-RU","mainEntityOfPage":{"@id":"https:\/\/otus.ru\/journal\/oop-na-c-pishem-videopleer\/#webpage"},"isPartOf":{"@id":"https:\/\/otus.ru\/journal\/oop-na-c-pishem-videopleer\/#webpage"},"articleSection":"\u041f\u0440\u043e IT, \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435, \u0443\u0440\u043e\u043a"},{"@type":"BreadcrumbList","@id":"https:\/\/otus.ru\/journal\/oop-na-c-pishem-videopleer\/#breadcrumblist","itemListElement":[{"@type":"ListItem","@id":"https:\/\/otus.ru\/journal\/#listItem","position":1,"name":"\u0413\u043b\u0430\u0432\u043d\u0430\u044f \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430","item":"https:\/\/otus.ru\/journal\/","nextItem":"https:\/\/otus.ru\/journal\/oop-na-c-pishem-videopleer\/#listItem"},{"@type":"ListItem","@id":"https:\/\/otus.ru\/journal\/oop-na-c-pishem-videopleer\/#listItem","position":2,"name":"\u041e\u041e\u041f \u043d\u0430 C: \u043f\u0438\u0448\u0435\u043c \u0432\u0438\u0434\u0435\u043e\u043f\u043b\u0435\u0435\u0440","previousItem":"https:\/\/otus.ru\/journal\/#listItem"}]},{"@type":"Organization","@id":"https:\/\/otus.ru\/journal\/#organization","name":"\u041e\u0442\u0443\u0441 \u043e\u043d\u043b\u0430\u0439\u043d-\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u0435","url":"https:\/\/otus.ru\/journal\/","sameAs":["https:\/\/www.youtube.com\/channel\/UCetgtvy93o3i3CvyGXKFU3g"],"contactPoint":{"@type":"ContactPoint","telephone":"+74999389202","contactType":"Customer Support"}},{"@type":"Person","@id":"https:\/\/otus.ru\/journal\/author\/k-moseenkova\/#author","url":"https:\/\/otus.ru\/journal\/author\/k-moseenkova\/","name":"K. Moseenkova","image":{"@type":"ImageObject","@id":"https:\/\/otus.ru\/journal\/oop-na-c-pishem-videopleer\/#authorImage","url":"https:\/\/secure.gravatar.com\/avatar\/5bcd16ae9d4759f7841464ca0c13ba63?s=96&d=mm&r=g","width":96,"height":96,"caption":"K. Moseenkova"}},{"@type":"WebPage","@id":"https:\/\/otus.ru\/journal\/oop-na-c-pishem-videopleer\/#webpage","url":"https:\/\/otus.ru\/journal\/oop-na-c-pishem-videopleer\/","name":"\u041e\u041e\u041f \u043d\u0430 C: \u043f\u0438\u0448\u0435\u043c \u0432\u0438\u0434\u0435\u043e\u043f\u043b\u0435\u0435\u0440 OTUS","description":"\u0412 \u044d\u0442\u043e\u043c \u0443\u0440\u043e\u043a\u0435 \u043c\u044b \u0440\u0430\u0441\u0441\u043c\u043e\u0442\u0440\u0438\u043c \u0438\u0441\u0445\u043e\u0434\u043d\u044b\u0439 \u043a\u043e\u0434 \u0443\u0442\u0438\u043b\u0438\u0442\u044b, \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u044e\u0449\u0435\u0439 \u043f\u0440\u043e\u0438\u0433\u0440\u044b\u0432\u0430\u0442\u044c \u0440\u043e\u043b\u0438\u043a\u0438 \u0441 \u0432\u0438\u0434\u0435\u043e\u0445\u043e\u0441\u0442\u0438\u043d\u0433\u0430 Vimeo. \u041d\u0430 \u043f\u0440\u0435\u0434\u044b\u0434\u0443\u0449\u0435\u043c \u043e\u0442\u043a\u0440\u044b\u0442\u043e\u043c \u0443\u0440\u043e\u043a\u0435, \u0441\u043e\u0441\u0442\u043e\u044f\u0432\u0448\u0435\u043c\u0441\u044f \u0432 \u0440\u0430\u043c\u043a\u0430\u0445 \u043e\u043d\u043b\u0430\u0439\u043d-\u043a\u0443\u0440\u0441\u0430 \u00ab\u041f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u0438\u0441\u0442 \u0421\u00bb \u0431\u044b\u043b\u0430 \u0441\u043e\u0437\u0434\u0430\u043d\u0430 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u0430, \u0430\u043d\u0430\u043b\u043e\u0433\u0438\u0447\u043d\u0430\u044f \u0438\u0437\u0432\u0435\u0441\u0442\u043d\u043e\u043c\u0443 \u043e\u043f\u0435\u043d\u0441\u043e\u0440\u0441\u043d\u043e\u043c\u0443 \u043f\u0440\u043e\u0434\u0443\u043a\u0442\u0443 youtube-dl, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0437\u0430\u043d\u0438\u043c\u0430\u0435\u0442\u0441\u044f \u0441\u043a\u0430\u0447\u0438\u0432\u0430\u043d\u0438\u0435\u043c \u0444\u0430\u0439\u043b\u043e\u0432 \u0441 \u0440\u0430\u0437\u043b\u0438\u0447\u043d\u044b\u0445 \u0432\u0438\u0434\u0435\u043e\u0445\u043e\u0441\u0442\u0438\u043d\u0433\u043e\u0432. Youtube-dl \u043f\u0440\u0438\u043d\u0438\u043c\u0430\u0435\u0442 \u043d\u0430 \u0432\u0445\u043e\u0434 \u0441\u0441\u044b\u043b\u043a\u0443 \u043d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 \u0441 \u0432\u0438\u0434\u0435\u043e \u0438 \u0441\u043a\u0430\u0447\u0438\u0432\u0430\u0435\u0442 \u0432\u0438\u0434\u0435\u043e\u0444\u0430\u0439\u043b \u0434\u043b\u044f \u043f\u043e\u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u0433\u043e \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0430","inLanguage":"ru-RU","isPartOf":{"@id":"https:\/\/otus.ru\/journal\/#website"},"breadcrumb":{"@id":"https:\/\/otus.ru\/journal\/oop-na-c-pishem-videopleer\/#breadcrumblist"},"author":{"@id":"https:\/\/otus.ru\/journal\/author\/k-moseenkova\/#author"},"creator":{"@id":"https:\/\/otus.ru\/journal\/author\/k-moseenkova\/#author"},"image":{"@type":"ImageObject","url":"https:\/\/otus.ru\/journal\/wp-content\/uploads\/2021\/05\/oj-1080x720-kopiya-1-1.png","@id":"https:\/\/otus.ru\/journal\/oop-na-c-pishem-videopleer\/#mainImage","width":1080,"height":720},"primaryImageOfPage":{"@id":"https:\/\/otus.ru\/journal\/oop-na-c-pishem-videopleer\/#mainImage"},"datePublished":"2021-05-24T14:51:07+00:00","dateModified":"2021-05-24T17:34:06+00:00"},{"@type":"WebSite","@id":"https:\/\/otus.ru\/journal\/#website","url":"https:\/\/otus.ru\/journal\/","name":"OTUS JOURNAL","description":"Blog about IT","inLanguage":"ru-RU","publisher":{"@id":"https:\/\/otus.ru\/journal\/#organization"}}]}
</script>
<!-- All in One SEO -->
<link rel='dns-prefetch' href='//otus.ru' />
<link rel='dns-prefetch' href='//fonts.googleapis.com' />
<link rel='stylesheet' id='wp-block-library-css' href='https://otus.ru/journal/wp-includes/css/dist/block-library/style.min.css?ver=6.4.7' type='text/css' media='all' />
<style id='classic-theme-styles-inline-css' type='text/css'>
/*! This file is auto-generated */
.wp-block-button__link{color:#fff;background-color:#32373c;border-radius:9999px;box-shadow:none;text-decoration:none;padding:calc(.667em + 2px) calc(1.333em + 2px);font-size:1.125em}.wp-block-file__button{background:#32373c;color:#fff;text-decoration:none}
</style>
<style id='global-styles-inline-css' type='text/css'>
body{--wp--preset--color--black: #000000;--wp--preset--color--cyan-bluish-gray: #abb8c3;--wp--preset--color--white: #ffffff;--wp--preset--color--pale-pink: #f78da7;--wp--preset--color--vivid-red: #cf2e2e;--wp--preset--color--luminous-vivid-orange: #ff6900;--wp--preset--color--luminous-vivid-amber: #fcb900;--wp--preset--color--light-green-cyan: #7bdcb5;--wp--preset--color--vivid-green-cyan: #00d084;--wp--preset--color--pale-cyan-blue: #8ed1fc;--wp--preset--color--vivid-cyan-blue: #0693e3;--wp--preset--color--vivid-purple: #9b51e0;--wp--preset--gradient--vivid-cyan-blue-to-vivid-purple: linear-gradient(135deg,rgba(6,147,227,1) 0%,rgb(155,81,224) 100%);--wp--preset--gradient--light-green-cyan-to-vivid-green-cyan: linear-gradient(135deg,rgb(122,220,180) 0%,rgb(0,208,130) 100%);--wp--preset--gradient--luminous-vivid-amber-to-luminous-vivid-orange: linear-gradient(135deg,rgba(252,185,0,1) 0%,rgba(255,105,0,1) 100%);--wp--preset--gradient--luminous-vivid-orange-to-vivid-red: linear-gradient(135deg,rgba(255,105,0,1) 0%,rgb(207,46,46) 100%);--wp--preset--gradient--very-light-gray-to-cyan-bluish-gray: linear-gradient(135deg,rgb(238,238,238) 0%,rgb(169,184,195) 100%);--wp--preset--gradient--cool-to-warm-spectrum: linear-gradient(135deg,rgb(74,234,220) 0%,rgb(151,120,209) 20%,rgb(207,42,186) 40%,rgb(238,44,130) 60%,rgb(251,105,98) 80%,rgb(254,248,76) 100%);--wp--preset--gradient--blush-light-purple: linear-gradient(135deg,rgb(255,206,236) 0%,rgb(152,150,240) 100%);--wp--preset--gradient--blush-bordeaux: linear-gradient(135deg,rgb(254,205,165) 0%,rgb(254,45,45) 50%,rgb(107,0,62) 100%);--wp--preset--gradient--luminous-dusk: linear-gradient(135deg,rgb(255,203,112) 0%,rgb(199,81,192) 50%,rgb(65,88,208) 100%);--wp--preset--gradient--pale-ocean: linear-gradient(135deg,rgb(255,245,203) 0%,rgb(182,227,212) 50%,rgb(51,167,181) 100%);--wp--preset--gradient--electric-grass: linear-gradient(135deg,rgb(202,248,128) 0%,rgb(113,206,126) 100%);--wp--preset--gradient--midnight: linear-gradient(135deg,rgb(2,3,129) 0%,rgb(40,116,252) 100%);--wp--preset--font-size--small: 13px;--wp--preset--font-size--medium: 20px;--wp--preset--font-size--large: 36px;--wp--preset--font-size--x-large: 42px;--wp--preset--spacing--20: 0.44rem;--wp--preset--spacing--30: 0.67rem;--wp--preset--spacing--40: 1rem;--wp--preset--spacing--50: 1.5rem;--wp--preset--spacing--60: 2.25rem;--wp--preset--spacing--70: 3.38rem;--wp--preset--spacing--80: 5.06rem;--wp--preset--shadow--natural: 6px 6px 9px rgba(0, 0, 0, 0.2);--wp--preset--shadow--deep: 12px 12px 50px rgba(0, 0, 0, 0.4);--wp--preset--shadow--sharp: 6px 6px 0px rgba(0, 0, 0, 0.2);--wp--preset--shadow--outlined: 6px 6px 0px -3px rgba(255, 255, 255, 1), 6px 6px rgba(0, 0, 0, 1);--wp--preset--shadow--crisp: 6px 6px 0px rgba(0, 0, 0, 1);}:where(.is-layout-flex){gap: 0.5em;}:where(.is-layout-grid){gap: 0.5em;}body .is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}body .is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}body .is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}body .is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}body .is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}body .is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}body .is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){max-width: var(--wp--style--global--content-size);margin-left: auto !important;margin-right: auto !important;}body .is-layout-constrained > .alignwide{max-width: var(--wp--style--global--wide-size);}body .is-layout-flex{display: flex;}body .is-layout-flex{flex-wrap: wrap;align-items: center;}body .is-layout-flex > *{margin: 0;}body .is-layout-grid{display: grid;}body .is-layout-grid > *{margin: 0;}:where(.wp-block-columns.is-layout-flex){gap: 2em;}:where(.wp-block-columns.is-layout-grid){gap: 2em;}:where(.wp-block-post-template.is-layout-flex){gap: 1.25em;}:where(.wp-block-post-template.is-layout-grid){gap: 1.25em;}.has-black-color{color: var(--wp--preset--color--black) !important;}.has-cyan-bluish-gray-color{color: var(--wp--preset--color--cyan-bluish-gray) !important;}.has-white-color{color: var(--wp--preset--color--white) !important;}.has-pale-pink-color{color: var(--wp--preset--color--pale-pink) !important;}.has-vivid-red-color{color: var(--wp--preset--color--vivid-red) !important;}.has-luminous-vivid-orange-color{color: var(--wp--preset--color--luminous-vivid-orange) !important;}.has-luminous-vivid-amber-color{color: var(--wp--preset--color--luminous-vivid-amber) !important;}.has-light-green-cyan-color{color: var(--wp--preset--color--light-green-cyan) !important;}.has-vivid-green-cyan-color{color: var(--wp--preset--color--vivid-green-cyan) !important;}.has-pale-cyan-blue-color{color: var(--wp--preset--color--pale-cyan-blue) !important;}.has-vivid-cyan-blue-color{color: var(--wp--preset--color--vivid-cyan-blue) !important;}.has-vivid-purple-color{color: var(--wp--preset--color--vivid-purple) !important;}.has-black-background-color{background-color: var(--wp--preset--color--black) !important;}.has-cyan-bluish-gray-background-color{background-color: var(--wp--preset--color--cyan-bluish-gray) !important;}.has-white-background-color{background-color: var(--wp--preset--color--white) !important;}.has-pale-pink-background-color{background-color: var(--wp--preset--color--pale-pink) !important;}.has-vivid-red-background-color{background-color: var(--wp--preset--color--vivid-red) !important;}.has-luminous-vivid-orange-background-color{background-color: var(--wp--preset--color--luminous-vivid-orange) !important;}.has-luminous-vivid-amber-background-color{background-color: var(--wp--preset--color--luminous-vivid-amber) !important;}.has-light-green-cyan-background-color{background-color: var(--wp--preset--color--light-green-cyan) !important;}.has-vivid-green-cyan-background-color{background-color: var(--wp--preset--color--vivid-green-cyan) !important;}.has-pale-cyan-blue-background-color{background-color: var(--wp--preset--color--pale-cyan-blue) !important;}.has-vivid-cyan-blue-background-color{background-color: var(--wp--preset--color--vivid-cyan-blue) !important;}.has-vivid-purple-background-color{background-color: var(--wp--preset--color--vivid-purple) !important;}.has-black-border-color{border-color: var(--wp--preset--color--black) !important;}.has-cyan-bluish-gray-border-color{border-color: var(--wp--preset--color--cyan-bluish-gray) !important;}.has-white-border-color{border-color: var(--wp--preset--color--white) !important;}.has-pale-pink-border-color{border-color: var(--wp--preset--color--pale-pink) !important;}.has-vivid-red-border-color{border-color: var(--wp--preset--color--vivid-red) !important;}.has-luminous-vivid-orange-border-color{border-color: var(--wp--preset--color--luminous-vivid-orange) !important;}.has-luminous-vivid-amber-border-color{border-color: var(--wp--preset--color--luminous-vivid-amber) !important;}.has-light-green-cyan-border-color{border-color: var(--wp--preset--color--light-green-cyan) !important;}.has-vivid-green-cyan-border-color{border-color: var(--wp--preset--color--vivid-green-cyan) !important;}.has-pale-cyan-blue-border-color{border-color: var(--wp--preset--color--pale-cyan-blue) !important;}.has-vivid-cyan-blue-border-color{border-color: var(--wp--preset--color--vivid-cyan-blue) !important;}.has-vivid-purple-border-color{border-color: var(--wp--preset--color--vivid-purple) !important;}.has-vivid-cyan-blue-to-vivid-purple-gradient-background{background: var(--wp--preset--gradient--vivid-cyan-blue-to-vivid-purple) !important;}.has-light-green-cyan-to-vivid-green-cyan-gradient-background{background: var(--wp--preset--gradient--light-green-cyan-to-vivid-green-cyan) !important;}.has-luminous-vivid-amber-to-luminous-vivid-orange-gradient-background{background: var(--wp--preset--gradient--luminous-vivid-amber-to-luminous-vivid-orange) !important;}.has-luminous-vivid-orange-to-vivid-red-gradient-background{background: var(--wp--preset--gradient--luminous-vivid-orange-to-vivid-red) !important;}.has-very-light-gray-to-cyan-bluish-gray-gradient-background{background: var(--wp--preset--gradient--very-light-gray-to-cyan-bluish-gray) !important;}.has-cool-to-warm-spectrum-gradient-background{background: var(--wp--preset--gradient--cool-to-warm-spectrum) !important;}.has-blush-light-purple-gradient-background{background: var(--wp--preset--gradient--blush-light-purple) !important;}.has-blush-bordeaux-gradient-background{background: var(--wp--preset--gradient--blush-bordeaux) !important;}.has-luminous-dusk-gradient-background{background: var(--wp--preset--gradient--luminous-dusk) !important;}.has-pale-ocean-gradient-background{background: var(--wp--preset--gradient--pale-ocean) !important;}.has-electric-grass-gradient-background{background: var(--wp--preset--gradient--electric-grass) !important;}.has-midnight-gradient-background{background: var(--wp--preset--gradient--midnight) !important;}.has-small-font-size{font-size: var(--wp--preset--font-size--small) !important;}.has-medium-font-size{font-size: var(--wp--preset--font-size--medium) !important;}.has-large-font-size{font-size: var(--wp--preset--font-size--large) !important;}.has-x-large-font-size{font-size: var(--wp--preset--font-size--x-large) !important;}
.wp-block-navigation a:where(:not(.wp-element-button)){color: inherit;}
:where(.wp-block-post-template.is-layout-flex){gap: 1.25em;}:where(.wp-block-post-template.is-layout-grid){gap: 1.25em;}
:where(.wp-block-columns.is-layout-flex){gap: 2em;}:where(.wp-block-columns.is-layout-grid){gap: 2em;}
.wp-block-pullquote{font-size: 1.5em;line-height: 1.6;}
</style>
<link rel='stylesheet' id='wbcr-comments-plus-url-span-css' href='https://otus.ru/journal/wp-content/plugins/clearfy/components/comments-plus/assets/css/url-span.css?ver=2.2.0' type='text/css' media='all' />
<link rel='stylesheet' id='wpel-style-css' href='https://otus.ru/journal/wp-content/plugins/wp-external-links/public/css/wpel.css?ver=2.59' type='text/css' media='all' />
<link rel='stylesheet' id='ez-toc-css' href='https://otus.ru/journal/wp-content/plugins/easy-table-of-contents/assets/css/screen.min.css?ver=2.0.61' type='text/css' media='all' />
<style id='ez-toc-inline-css' type='text/css'>
div#ez-toc-container .ez-toc-title {font-size: 120%;}div#ez-toc-container .ez-toc-title {font-weight: 500;}div#ez-toc-container ul li {font-size: 95%;}div#ez-toc-container nav ul ul li {font-size: 90%;}
.ez-toc-container-direction {direction: ltr;}.ez-toc-counter ul{counter-reset: item ;}.ez-toc-counter nav ul li a::before {content: counters(item, ".", decimal) ". ";display: inline-block;counter-increment: item;flex-grow: 0;flex-shrink: 0;margin-right: .2em; float: left; }.ez-toc-widget-direction {direction: ltr;}.ez-toc-widget-container ul{counter-reset: item ;}.ez-toc-widget-container nav ul li a::before {content: counters(item, ".", decimal) ". ";display: inline-block;counter-increment: item;flex-grow: 0;flex-shrink: 0;margin-right: .2em; float: left; }
</style>
<link rel='stylesheet' id='contentberg-fonts-css' href='https://fonts.googleapis.com/css?family=Roboto%3A400%2C500%2C700%7CPT+Serif%3A400%2C400i%2C600%7CIBM+Plex+Serif%3A500' type='text/css' media='all' />
<link rel='stylesheet' id='contentberg-core-css' href='https://otus.ru/journal/wp-content/themes/contentberg/style.css?ver=1.8.3' type='text/css' media='all' />
<link rel='stylesheet' id='contentberg-lightbox-css' href='https://otus.ru/journal/wp-content/themes/contentberg/css/lightbox.css?ver=1.8.3' type='text/css' media='all' />
<link rel='stylesheet' id='font-awesome-css' href='https://otus.ru/journal/wp-content/themes/contentberg/css/fontawesome/css/font-awesome.min.css?ver=1.8.3' type='text/css' media='all' />
<script type="text/javascript" id="breeze-prefetch-js-extra">
/* <![CDATA[ */
var breeze_prefetch = {"local_url":"https:\/\/otus.ru\/journal","ignore_remote_prefetch":"1","ignore_list":["\/wp-admin\/"]};
/* ]]> */
</script>
<script type="text/javascript" src="https://otus.ru/journal/wp-content/plugins/breeze/assets/js/js-front-end/breeze-prefetch-links.min.js" id="breeze-prefetch-js"></script>
<script type="text/javascript" src="https://otus.ru/journal/wp-includes/js/jquery/jquery.min.js" id="jquery-core-js"></script>
<script type="text/javascript" src="https://otus.ru/journal/wp-includes/js/jquery/jquery-migrate.min.js" id="jquery-migrate-js"></script>
<script type="text/javascript" src="https://otus.ru/journal/wp-content/themes/contentberg/js/lazysizes.js" id="lazysizes-js"></script>
<link rel="https://api.w.org/" href="https://otus.ru/journal/wp-json/" /><link rel="alternate" type="application/json" href="https://otus.ru/journal/wp-json/wp/v2/posts/1316" /><link rel='shortlink' href='https://otus.ru/journal/?p=1316' />
<link rel="alternate" type="application/json+oembed" href="https://otus.ru/journal/wp-json/oembed/1.0/embed?url=https%3A%2F%2Fotus.ru%2Fjournal%2Foop-na-c-pishem-videopleer%2F" />
<link rel="alternate" type="text/xml+oembed" href="https://otus.ru/journal/wp-json/oembed/1.0/embed?url=https%3A%2F%2Fotus.ru%2Fjournal%2Foop-na-c-pishem-videopleer%2F&format=xml" />
<script>var Sphere_Plugin = {"ajaxurl":"https:\/\/otus.ru\/journal\/wp-admin\/admin-ajax.php"};</script><link rel="icon" href="https://otus.ru/journal/wp-content/uploads/2020/11/cropped-OTUS_logo_OTUS-COMP-LOGO-WHITE-1-32x32.png" sizes="32x32" />
<link rel="icon" href="https://otus.ru/journal/wp-content/uploads/2020/11/cropped-OTUS_logo_OTUS-COMP-LOGO-WHITE-1-192x192.png" sizes="192x192" />
<link rel="apple-touch-icon" href="https://otus.ru/journal/wp-content/uploads/2020/11/cropped-OTUS_logo_OTUS-COMP-LOGO-WHITE-1-180x180.png" />
<meta name="msapplication-TileImage" content="https://otus.ru/journal/wp-content/uploads/2020/11/cropped-OTUS_logo_OTUS-COMP-LOGO-WHITE-1-270x270.png" />
<style type="text/css" id="wp-custom-css">
#menu-item-10406 .wpel-icon {
display: none;
}
#menu-item-10407 .wpel-icon {
display: none;
}
.otus-login-site a .wpel-icon {
display: none;
}
.menu-menju-navykov-container a .wpel-icon {
display: none;
}
.otus-login-site a
{
background: #ffd709;
border-radius: 12px;
color: #0f0f10;
font-size: 14px;
font-weight: 700;
line-height: 20px;
display: block;
text-align: center;
padding: 8px 25px;
}
.main-footer.dark {
background: linear-gradient(90deg, #a64fc5, #4f54e6);
border-color: transparent;
}
.main-footer.bold .copyright {
color: #fff;
}
.main-footer.bold .to-top i {
color: #fff;
}
.main-footer.bold .back-to-top {
color: #fff;
}
.nav__scroll {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.scrollable-menu .menu {
display: flex;
}
.nav__scroll
{
background: linear-gradient(90deg, #a64fc5, #4f54e6);
}
.scrollable-menu .menu .menu-item {
flex: 0 0 auto;
padding: 15px 15px;
}
.scrollable-menu .menu .menu-item a {
color: #fff;
}
.nav__scroll::-webkit-scrollbar{background-color:#fff;height:5px;}
.nav__scroll::-webkit-scrollbar-thumb{background-color:#dcdcdc;}
.nav__scroll::-webkit-scrollbar-track{-webkit-border-radius:0;border-radius:0;background-color:#fff;}/
body {
min-width: 320px;
}
.banner-click img {
margin: 0 auto;
display: block;
}
.banner-click {
cursor: pointer;
}
.banner-footer-area {
margin-bottom: 20px;
}
.banner-left-area {
margin-top: 40px;
} </style>
<!--Start VDZ Yandex Metrika Plugin-->
<!-- Yandex.Metrika counter --><script type="text/javascript" >(function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};m[i].l=1*new Date();k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)})(window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym");ym(34531570, "init", {clickmap:true, trackLinks:true, accurateTrackBounce:true, webvisor:true, trackHash:true, ecommerce:"dataLayer"});</script>
<noscript><div><img src="https://mc.yandex.ru/watch/34531570" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
<!-- /Yandex.Metrika counter --><!--START ADD EVENTS FROM CF7--><script type='text/javascript'>document.addEventListener( 'wpcf7submit', function( event ) {
//event.detail.contactFormId;
if(ym){
//console.log(event.detail);
ym(34531570, 'reachGoal', 'VDZ_SEND_CONTACT_FORM_7');
ym(34531570, 'params', {
page_url: window.location.href,
status: event.detail.status,
locale: event.detail.contactFormLocale,
form_id: event.detail.contactFormId,
});
}
}, false );
</script><!--END ADD EVENTS FROM CF7-->
<!--End VDZ Yandex Metrika Plugin-->
</head>
<body class="post-template-default single single-post postid-1316 single-format-standard right-sidebar lazy-normal has-lb">
<div class="main-wrap">
<header id="main-head" class="main-head head-nav-below has-search-modal simple simple-boxed">
<div class="inner inner-head" data-sticky-bar="0">
<div class="wrap cf wrap-head">
<div class="left-contain">
<span class="mobile-nav"><i class="fa fa-bars"></i></span>
<div class="title">
<a href="https://otus.ru/journal/" title="OTUS JOURNAL" rel="home" data-wpel-link="internal">
<span class="text-logo"><img src="/journal/wp-content/themes/contentberg/img/logo_site.svg" alt="OTUS JOURNAL"></span>
</a>
</div>
</div>
<div class="navigation-wrap inline">
<nav class="navigation inline simple light" data-sticky-bar="0">
<div class="menu-rubriki-container"><ul id="menu-rubriki" class="menu"><li id="menu-item-109" class="menu-item menu-item-type-taxonomy menu-item-object-category current-post-ancestor current-menu-parent current-post-parent menu-cat-1 menu-item-109"><a href="https://otus.ru/journal/category/pro-it/" data-wpel-link="internal"><span>Про IT</span></a></li>
<li id="menu-item-113" class="menu-item menu-item-type-taxonomy menu-item-object-category menu-cat-4 menu-item-113"><a href="https://otus.ru/journal/category/polza/" data-wpel-link="internal"><span>Полезное</span></a></li>
<li id="menu-item-114" class="menu-item menu-item-type-taxonomy menu-item-object-category menu-cat-3 menu-item-114"><a href="https://otus.ru/journal/category/lifestyle/" data-wpel-link="internal"><span>Лайфстайл</span></a></li>
<li id="menu-item-10406" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-10406"><a href="https://otus.ru/catalog/courses" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><span>Обучение</span><span class="wpel-icon wpel-image wpel-icon-6"></span></a></li>
<li id="menu-item-10407" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-10407"><a href="https://otus.ru/about" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><span>Информация</span><span class="wpel-icon wpel-image wpel-icon-6"></span></a></li>
</ul></div> </nav>
</div>
<div class="actions">
<div class="otus-login-site">
<a href="https://otus.ru/login/" target="_blank" data-wpel-link="external" rel="nofollow external noopener noreferrer" class="wpel-icon-right">Войти<span class="wpel-icon wpel-image wpel-icon-6"></span></a>
</div>
<a href="#" title="Search" class="search-link"><i class="fa fa-search"></i></a>
</div>
</div>
</div>
</header> <!-- .main-head -->
<div class="nav nav_disable nav_colored nav_transparent course-categories__nav nav__scroll ">
<div class="container wrap">
<div class="links inline simple light scrollable-menu">
<div class="menu-menju-navykov-container"><ul id="menu-menju-navykov" class="menu"><li id="menu-item-10413" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-10413"><a href="https://otus.ru/categories/programming/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">Программирование<span class="wpel-icon wpel-image wpel-icon-6"></span></a></li>
<li id="menu-item-10414" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-10414"><a href="https://otus.ru/categories/architecture/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">Архитектура<span class="wpel-icon wpel-image wpel-icon-6"></span></a></li>
<li id="menu-item-10415" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-10415"><a href="https://otus.ru/categories/operations/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">Инфраструктура<span class="wpel-icon wpel-image wpel-icon-6"></span></a></li>
<li id="menu-item-10416" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-10416"><a href="https://otus.ru/categories/information-security-courses/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">Безопасность<span class="wpel-icon wpel-image wpel-icon-6"></span></a></li>
<li id="menu-item-10417" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-10417"><a href="https://otus.ru/categories/data-science/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">Data Science<span class="wpel-icon wpel-image wpel-icon-6"></span></a></li>
<li id="menu-item-10418" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-10418"><a href="https://otus.ru/categories/gamedev/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">GameDev<span class="wpel-icon wpel-image wpel-icon-6"></span></a></li>
<li id="menu-item-10419" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-10419"><a href="https://otus.ru/categories/marketing-business/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">Управление<span class="wpel-icon wpel-image wpel-icon-6"></span></a></li>
<li id="menu-item-10420" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-10420"><a href="https://otus.ru/categories/analytics/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">Аналитика и анализ<span class="wpel-icon wpel-image wpel-icon-6"></span></a></li>
<li id="menu-item-10421" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-10421"><a href="https://otus.ru/categories/testing/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">Тестирование<span class="wpel-icon wpel-image wpel-icon-6"></span></a></li>
</ul></div> </div>
</div>
</div>
<div class="main wrap">
<div class="ts-row cf">
<div class="col-8 main-content cf">
<article id="post-1316" class="the-post post-1316 post type-post status-publish format-standard has-post-thumbnail category-pro-it tag-programmirovanie tag-urok">
<header class="post-header the-post-header cf">
<div class="post-meta the-post-meta">
<span class="post-cat">
<a href="https://otus.ru/journal/category/pro-it/" class="category" data-wpel-link="internal">Про IT</a>
</span>
<h1 class="post-title">
ООП на C: пишем видеоплеер
</h1>
<a href="https://otus.ru/journal/oop-na-c-pishem-videopleer/" class="date-link" data-wpel-link="internal"><time class="post-date">24 мая, 2021</time></a>
</div>
<div class="featured">
<a href="https://otus.ru/journal/wp-content/uploads/2021/05/oj-1080x720-kopiya-1-1.png" class="image-link" data-wpel-link="internal"><img width="770" height="515" src="data:image/svg+xml,%3Csvg%20viewBox%3D%270%200%20770%20515%27%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%3E%3C%2Fsvg%3E" class="attachment-contentberg-main size-contentberg-main lazyload wp-post-image" alt="ООП на C: пишем видеоплеер" title="ООП на C: пишем видеоплеер" decoding="async" fetchpriority="high" data-srcset="https://otus.ru/journal/wp-content/uploads/2021/05/oj-1080x720-kopiya-1-1-770x515.png 770w, https://otus.ru/journal/wp-content/uploads/2021/05/oj-1080x720-kopiya-1-1-300x200.png 300w, https://otus.ru/journal/wp-content/uploads/2021/05/oj-1080x720-kopiya-1-1-1024x683.png 1024w, https://otus.ru/journal/wp-content/uploads/2021/05/oj-1080x720-kopiya-1-1-150x100.png 150w, https://otus.ru/journal/wp-content/uploads/2021/05/oj-1080x720-kopiya-1-1-270x180.png 270w" data-src="https://otus.ru/journal/wp-content/uploads/2021/05/oj-1080x720-kopiya-1-1-770x515.png" data-sizes="(max-width: 770px) 100vw, 770px" /> </a>
</div>
</header><!-- .post-header -->
<div class="post-content description cf entry-content content-normal">
<p>В этом уроке мы рассмотрим исходный код утилиты, позволяющей проигрывать ролики с видеохостинга <a href="https://vimeo.com" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">Vimeo<span class="wpel-icon wpel-image wpel-icon-6"></span></a>. На <a href="http://youtu.be/46u8KAILwew" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">предыдущем открытом уроке<span class="wpel-icon wpel-image wpel-icon-6"></span></a>, состоявшемся в рамках онлайн-курса <a href="https://otus.ru/lessons/dev_c/?utm_source=oj&utm_medium=affilate&utm_campaign=dev_c&utm_term=24.05.2021&mxm=[[hash_metrika]]&relogin=True&token=[[token]]" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">«Программист С»<span class="wpel-icon wpel-image wpel-icon-6"></span></a> была создана программа, аналогичная известному опенсорсному продукту <a href="https://github.com/ytdl-org/youtube-dl" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">youtube-dl<span class="wpel-icon wpel-image wpel-icon-6"></span></a>, который занимается скачиванием файлов с различных видеохостингов. Youtube-dl принимает на вход ссылку на страницу с видео и скачивает видеофайл для последующего локального просмотра любимым плеером. Мы используем часть кода с прошлого занятия, которая получает адрес видеофайла с конкретной страницы Vimeo. На этот раз мы увидим окно плеера с видео (а в идеале — и с аудио), и для этого мы будем использовать фреймворк GStreamer.</p>
<p>В случаях, когда в программе нужна обработка мультимедийных данных, будь то видео или аудио, в независимости от используемого языка (C, C++ или что-то другое) есть два наиболее распространённых варианта для добавления такой функциональности: <a href="https://gstreamer.freedesktop.org" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">GStreamer<span class="wpel-icon wpel-image wpel-icon-6"></span></a> и <a href="https://ffmpeg.org" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">FFMpeg<span class="wpel-icon wpel-image wpel-icon-6"></span></a>.</p>
<figure class="wp-block-image"><img decoding="async" src="https://web-fluendo.s3.amazonaws.com/media/blog_images/2018/08/ffmpeg-gstreamer.jpg" alt="ООП на C: пишем видеоплеер"/></figure>
<p>И то, и другое — опенсорсные библиотеки, у обоих достаточно пермиссивные лицензии, то есть их можно использовать в любых коммерческих приложениях. Безусловно, есть ряд других решений, в том числе библиотека <a href="https://libav.org" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">libav<span class="wpel-icon wpel-image wpel-icon-6"></span></a>, которая является форком FFMpeg, но две вышеперечисленных библиотеки можно назвать наиболее популярными. В чём же различия между ними?</p>
<p>FFMpeg — сравнительно простая библиотека, достаточно использовать несколько её функций со стабильным API для того, чтобы, например, проиграть видеофайл, или последовательно получать из него кадр за кадром и работать с каждым из них как с отдельным изображением; для этого достаточно написать <a href="https://ffmpeg.org/doxygen/trunk/decode__video_8c_source.html" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">кусок кода длиной в полторы сотни строчек<span class="wpel-icon wpel-image wpel-icon-6"></span></a>. Проблема в том, что как только возникает нужда решить более сложную задачу, произвести некий нетривиальный анализ, или некоторое перекодирование, то всё становится намного сложнее — приходится лезть в исходники самой библиотеки и вызывать не экспортируемые наружу API, которые при следующих релизах библиотеки меняются или пропадают вовсе, и, как следствие, программа работает только с одной конкретной версией FFMpeg.</p>
<p>Резюмируя, если нужно сделать что-то простое, FFMpeg подойдёт, но для более сложных задач имеет смысл обратить взгляд на фреймворк GStreamer. Это не просто библиотека, а целый программный каркас, на который можно насаживать свои элементы, которые занимаются обработкой видео, аудио и прочей мультимедийной информации. По своему внутреннему устройству GStreamer внешне похож на механизм <a href="https://docs.microsoft.com/en-us/windows/win32/directshow/directshow-filters" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">фильтров DirectShow<span class="wpel-icon wpel-image wpel-icon-6"></span></a>, с которым можно столкнуться при программировании под Windows.</p>
<figure class="wp-block-image"><img decoding="async" src="https://upload.wikimedia.org/wikipedia/ru/0/0d/Dsstruct.jpg" alt="ООП на C: пишем видеоплеер"/></figure>
<p>DirectShow — это системная библиотека Windows для обработки видео. В частности, всякий раз, когда вы проигрываете видео на компьютере с данной ОС, под капотом у используемого вами видеоплеера создаётся так называемый pipeline (конвейер) из вышеупомянутых фильтров, и именно этот конвейер позволяет считывать, декодировать и отображать видео. GStreamer также предоставляет различные мультимедийные элементы и возможность комбинировать их в конвейер обработки.</p>
<p>Сам по себе GStreamer базируется на такой библиотеке, как <a href="https://developer.gnome.org/glib/stable" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">GLib<span class="wpel-icon wpel-image wpel-icon-6"></span></a>. Я люблю называть её «стандартной библиотекой на стероидах». Язык C разрабатывался в 70-х — 80-х годах прошлого века, и тогдашние взгляды на стандартную библиотеку языка были гораздо более минималистичными, буквально на уровне «можно открывать файлы — уже круто». Поэтому так сложилось, что в языке C стандартная библиотека, будем до конца честны, феноменально бедная, если сравнивать с более поздними языками — такими, как Python. В стандартной библиотеке последнего есть множество функций на любой случай жизни — можно скачать из интернета файл, закодировать данные в один из распространённых форматов вроде CSV или JSON, легко разобрать аргументы командной строки, и всё это — в стандартной поставке языка. В C же в подавляющем большинстве случаев придётся устанавливать сторонние библиотеки, чтобы сделать хоть что-либо.</p>
<p>Возвращаясь к GLib, это сторонняя библиотека, которая распространяется по пермиссивной лицензии LGPL. Она предоставляет множество различных подсистем, как то: <a href="https://developer.gnome.org/glib/stable/glib-Message-Logging.html" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">отладочное журналирование<span class="wpel-icon wpel-image wpel-icon-6"></span></a>, <a href="https://developer.gnome.org/glib/stable/glib-Error-Reporting.html" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">обработка ошибок<span class="wpel-icon wpel-image wpel-icon-6"></span></a>, <a href="https://developer.gnome.org/gio/stable/GSocketConnection.html" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">сетевые взаимодействия<span class="wpel-icon wpel-image wpel-icon-6"></span></a>, контейнерные <a href="https://developer.gnome.org/glib/stable/glib-data-types.html" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">типы данных<span class="wpel-icon wpel-image wpel-icon-6"></span></a> (хэш-таблица, красно-чёрное дерево и прочее, которых, конечно, нет в стандартной библиотеке C) и множество других.</p>
<p>Среди всего прочего, одна из подсистем GLib, называемая <a href="https://developer.gnome.org/gobject/stable" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">GObject<span class="wpel-icon wpel-image wpel-icon-6"></span></a>, добавляет возможность использовать ООП в чистом C. Эта подсистема добавляет ООП-шные возможности не на уровне языка, а на уровне библиотеки, то есть для их использования всё-таки придётся написать некоторое количество бойлерплейта — вспомогательного кода, который не несёт никакой логики, но требуется для работы GObject. Для этого в библиотеке есть набор макросов, которые генерируют код для поддержки объектов. В GObject поддерживается даже наследование (кроме множественного).</p>
<p>Для более подробного ознакомления с ООП на основе GLib рекомендуется обратиться к статье «<a href="https://habr.com/ru/post/418443" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">GObject: инкапсуляция, инстанциация, интроспекция<span class="wpel-icon wpel-image wpel-icon-6"></span></a>», являющейся первой из цикла статей на Хабр, подробно описывающего простой пример с наследованием GObject’ов.</p>
<p>ООП в GLib строится на базе обычных <a href="https://en.cppreference.com/w/c/language/struct" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">C-шных структур<span class="wpel-icon wpel-image wpel-icon-6"></span></a>, но в этих структурах также хранятся указатели на функции, которые используются в качестве виртуальных методов, то есть таких методов, которые могут быть переопределены в потомках класса при наследовании. Для каждого класса есть две структуры. Одна соответствует самому классу, и с ней происходит работа, когда речь идёт именно про класс всех возможных объектов и его поведение. Другая структура соответствует инстансу класса, то есть конкретному экземпляру. Для наследования используется интересный C-шный трюк. Рассмотрим его на примере из вышеупомянутой статьи:</p>
<pre class="wp-block-code"><code>struct _AnimalCatClass
{
GObjectClass parent_class; /* родительская классовая структура */
void (*say_meow) (AnimalCat*); /* виртуальный метод */
gpointer padding[10]; /* массив указателей */
};</code></pre>
<p>Мы определяем структуру, соответствующую нашему классу кошки <code>_AnimalCatClass</code>, и первым полем в ней мы определяем <code>parent_class</code>, имеющий тип <code>GObjectClass</code>, который также является структурой. Теперь за счёт этого поля и за счёт того, что оно идёт первым в определении нашей структуры, мы можем передавать её в те функции, которые ожидают в качестве параметра <code>GObjectClass</code>. Это работает благодаря расположению структуры в памяти: первые <code>sizeof(GObjectClass)</code> байт в нашей структуре полностью совпадают с <code>GObjectClass</code>, а всё, что идёт после них — это поля, специфичные для нашей структуры. В частности, <code>padding</code> — это резерв для последующих потомков нашей структуры, которые (возможно) будут переопределять виртуальные методы. Для того, чтобы их размер полностью совпадал с размером предка, у потомков размер поля <code>padding</code> нужно будет уменьшать за счёт добавления новых полей.</p>
<p>Далее мы определяем класс тигра _AnimalTiger:</p>
<pre class="wp-block-code"><code>struct _AnimalTiger
{
AnimalCat parent; /* обязательно первым полем должен идти экземпляр родительского объекта */
int speed; /* приватные данные */
};</code></pre>
<p>И снова используем вышеупомянутый трюк: самым первым полем в структуре идёт структура <code>AnimalCat</code>, и за счёт этого в любую функцию, ожидающую кота, мы сможем передать тигра 🐯</p>
<p>Вернёмся к GStreamer. Когда мы проигрываем некий видео- или аудиофайл с помощью GStreamer, под капотом у приложения, чем бы оно ни было — видеоплеером, аудиоплеером или нашим C-шным приложением, каждый раз создаётся граф декодирования. Он может выглядеть, например, следующим образом:</p>
<figure class="wp-block-image"><img decoding="async" src="https://i.stack.imgur.com/KCEEL.png" alt="ООП на C: пишем видеоплеер"/></figure>
<p>Центральная часть фреймворка GStreamer, представляемая этим графом — так называемый <em>pipeline</em>, конвейер. Отдельными ступенями этого конвейера являются экземпляры класса <a href="https://gstreamer.freedesktop.org/documentation/gstreamer/gstelement.html?gi-language=c" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><code>GstElement</code><span class="wpel-icon wpel-image wpel-icon-6"></span></a>, в терминологии фреймворка — <em>элементы</em>. Каждый элемент может быть одного из трёх типов.</p>
<ol><li>Это может быть элемент, порождающий медиаинформацию, такой элемент называется <em>source</em>, источник. В примере на картинке выше это <a href="https://gstreamer.freedesktop.org/documentation/coreelements/filesrc.html?gi-language=c" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><code>file-source</code><span class="wpel-icon wpel-image wpel-icon-6"></span></a>, считывающий информацию из файла на диске.</li><li>Это может быть так называемый <em>sink</em> (дословно «сток»), являющийся конечной точкой конвейера. Он либо отображает пользователю медиа-информацию, либо утилизирует её другим образом — например, раздаёт по сети в качестве сервера. В примере на картинке у нас в конвейере сразу два стока (да, так тоже можно было), один — <a href="https://gstreamer.freedesktop.org/documentation/autodetect/autoaudiosink.html?gi-language=c" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><code>audio-sink</code><span class="wpel-icon wpel-image wpel-icon-6"></span></a>, воспроизводящий аудио, другой — <a href="https://gstreamer.freedesktop.org/documentation/autodetect/autovideosink.html" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><code>video-sink</code><span class="wpel-icon wpel-image wpel-icon-6"></span></a>, отображающий видео.</li><li>Могут быть промежуточные элементы, называемые <em>фильтрами</em>. В примере у нас три разных фильтра: <a href="https://gstreamer.freedesktop.org/documentation/ogg/oggdemux.html?gi-language=c" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><code>ogg-demuxer</code><span class="wpel-icon wpel-image wpel-icon-6"></span></a>, <a href="https://gstreamer.freedesktop.org/documentation/vorbis/vorbisdec.html" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><code>vorbis-decoder</code><span class="wpel-icon wpel-image wpel-icon-6"></span></a> и <a href="https://gstreamer.freedesktop.org/documentation/theora/theoradec.html" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><code>theora-decoder</code><span class="wpel-icon wpel-image wpel-icon-6"></span></a>. Суть фильтров в том, что они принимают на вход медиаинформацию, преобразуют её тем или иным образом и отдают дальше по конвейеру. Так, <code>ogg-demuxer</code> не занимается обработкой самой аудио или видеоинформации, он попросту разбирает формат контейнера OGG и достаёт из него эту информацию. В качестве других подобных примеров можно упомянуть другие demuxer’ы: для распространённого формата MP4, различных потоковых форматов, таких, как RTMP и RTSP, и так далее — все эти форматы поддерживаются GStreamer. Также в примере есть фильтры <code>vorbis-decoder</code> и <code>theora-decoder</code>, которые занимаются декодированием медиаинформации: в подавляющем большинстве случаев она хранится в сжатом виде, так как без сжатия даже небольшие по таймингу фрагменты могут занимать огромные объёмы памяти вплоть до десятков и сотен гигабайт. Фильтры-декодеры занимаются тем, что разжимают эту информацию для её дальнейшей обработки.</li></ol>
<p>У каждого элемента есть способ соедиенения с другими элементами, называемый <em>pads</em>. Трудно подобрать адекватный перевод этого термина; отличительная особенность как GLib, так и GStreamer в том, что эти библиотеки широко используют локализацию и интернационализацию, то есть они по максимуму пытаются общаться с пользователем на его родном языке. Так вот, в отладочных журналах GStreamer при запуске программы с русскоязычной локалью можно встретить перевод термина <em>pad</em> как «контактное гнездо» — за неимением лучшего будем далее использовать этот вариант.</p>
<p>Изначально в конвейере элементы, как правило, не связаны друг с другом, если только программист не связал их эксплицитно в момент написания кода. Кроме того, каждый элемент, как правило, имеет различные контактные гнёзда, как входные, так и выходные, и каждый раз, когда GStreamer автоматически строит конвейер, он обнаруживает, каким оптимальным способом можно соединить друг с другом различные элементы. Этот процесс называется <em>caps negotiation</em>, что можно перевести как «переговоры о возможностях». Например, у <code>ogg-demuxer</code> есть два выходных контактных гнезда: для видео и для аудио, и мы не можем, например, взять его аудиовыход и соединить со входом декодера видео. Для работы механизма caps negotiation каждый элемент должен реализовать ряд виртуальных методов, вызываемых фреймворком во время построения конвейера для получения информации о контактных гнёздах элемента и поддерживаемых им типах контента, называемых <em>caps</em> (по-видимому, сокращение от capabilities, «возможности»), например, <code>"video/x-h264"</code> для видео, пожатого кодеком <a href="https://ru.wikipedia.org/wiki/H.264" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">H.264<span class="wpel-icon wpel-image wpel-icon-6"></span></a>. Формат, в котором задаются возможности, сильно похож на <a href="https://developer.mozilla.org/ru/docs/Web/HTTP/Basics_of_HTTP/MIME_types" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">MIME-типы<span class="wpel-icon wpel-image wpel-icon-6"></span></a>, которые можно часто встретить в web-программировании. Итак, GStreamer, в свою очередь, вызывает вышеуказанные методы и согласует между собой элементы в конвейере; если процесс согласования не удастся, фреймворк сообщит об ошибке.</p>
<p>Для написания программы с использованием GStreamer, например, плеера, либо плагина со своими кастомными элементами, есть следующие ресурсы.</p>
<ul><li><a href="https://gstreamer.freedesktop.org/documentation/?gi-language=c" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">Официальная документация GStreamer<span class="wpel-icon wpel-image wpel-icon-6"></span></a>, к сожалению, по большей части довольно спартанская — одни сухие описания классов и функций, ничего лишнего.</li><li><a href="https://gstreamer.freedesktop.org/documentation/plugin-development/index.html?gi-language=c" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">Официальное руководство по созданию своих плагинов<span class="wpel-icon wpel-image wpel-icon-6"></span></a>.</li></ul>
<p>Также в GLib существует <a href="https://developer.gnome.org/glib/stable/glib-Reference-counting.html" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">механизм подсчёта ссылок<span class="wpel-icon wpel-image wpel-icon-6"></span></a>, что несколько упрощает написание кода, в том числе с использованием GStreamer — не нужно беспокоиться о том, что буферы памяти, через которые передаётся медиаинформация в конвейере, не были удалены в нужный момент времени и таким образом создают утечку свободной памяти. По сути, GLib предоставляет некий рудиментарный lifetime management для GObject’ов, чуть более гибкий, чем C-шная модель с указателями и эксплицитным указанием всего и вся.</p>
<p>Хорошим стартом для нового проекта с использованием GStreamer будет заранее созданный авторами фреймворка бойлерплейт, <a href="https://gstreamer.freedesktop.org/documentation/plugin-development/basics/boiler.html?gi-language=c" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">упоминаемый<span class="wpel-icon wpel-image wpel-icon-6"></span></a> в официальном руководстве. Есть два варианта получения этого бойлерплейта. Первый — склонировать себе готовый репозиторий:</p>
<pre class="wp-block-code"><code>git clone https://gitlab.freedesktop.org/gstreamer/gst-template.git</code></pre>
<p>Этот вариант включает в себя довольно много кода, который можно счесть лишним — там есть C-шное приложение, использующее GStreamer, и шаблон простенького элемента.</p>
<p>Второй способ — использовать специальную утилиту <code>gst-element-maker</code>, входящую в пакет <a href="https://gstreamer.freedesktop.org/modules/gst-plugins-bad.html" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">gst-plugins-bad<span class="wpel-icon wpel-image wpel-icon-6"></span></a>. Однако у меня этот способ не заработал под несколькими разными дистрибутивами GNU/Linux — ни в одном из них мейнтейнеры не включили эту утилиту в готовый пакет, поэтому мне пришлось скомпилировать эту утилиту из исходников самому.</p>
<p>Итак, мы собираемся создать не полноразмерный плеер, а один небольшой source element конвейера GStreamer, который будет получать видео из ссылки на страницу на Vimeo и отдавать его для дальнейшей обработки.</p>
<p>Существуют два разных способа того, как наш source element будет отдавать свои данные в конвейер: pull-режим и push-режим, тяни или толкай 😃<br>В push-режиме источник сам генерирует события о том, что у него есть новые данные. В pull-режиме эти данные из него забираются явным образом по мере необходимости соединёнными с ним элементами. Pull модель лучше подходит для <code>file-source</code>, так как файл мы можем в произвольный момент времени проматывать и получать из него байты из произвольного места. Push-интерфейс больше подходит для источников, которые работают в live-режиме, например, элемент, который забирает аудиосигнал с микрофона, видео с вебкамеры, или сетевой поток по протоколу RTMP — во всех этих случаях мы не можем по запросу фреймворка «промотать» источник данных.</p>
<p>В нашем коде используется push-режим, так как он чуть проще в программировании. Можно было реализовать и в pull-режиме, добавить проматывание на произвольное место, синхронизацию для предотвращения «захлёбывания» и прочие необходимые для этого вещи, но код стал бы сложнее для восприятия.</p>
<p>Для источников, работающих в push-режиме, есть целый отдельный класс <a href="https://gstreamer.freedesktop.org/documentation/base/gstpushsrc.html?gi-language=c" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><code>GstPushSrc</code><span class="wpel-icon wpel-image wpel-icon-6"></span></a>, наследуемый (как и все прочие классы GStreamer) от <code>GObject</code>. Кроме того, в его цепочке наследования есть класс <a href="https://gstreamer.freedesktop.org/documentation/gstreamer/gstelement.html?gi-language=c" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><code>GstElement</code><span class="wpel-icon wpel-image wpel-icon-6"></span></a>, представляющий собой элементы конвейера, и <a href="https://gstreamer.freedesktop.org/documentation/base/gstbasesrc.html?gi-language=c" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><code>GstBaseSrc</code><span class="wpel-icon wpel-image wpel-icon-6"></span></a>, являющийся базовым для всех элементов-источников. <code>GstPushSrc</code> — специальный класс источника, работающий только в push-режиме; в принципе, push-источник можно написать, отнаследовавшись от <code>GstBaseSrc</code>, но для этого понадобится чуть больше бойлерплейта.</p>
<p>Перейдём к коду нашего элемента; его можно найти в <a href="https://gitlab.com/lockie/otus-video-player" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">данном репозитории<span class="wpel-icon wpel-image wpel-icon-6"></span></a>. Рассмотрим ключевые фрагменты различных файлов.</p>
<p>В корне репозитория есть несколько вспомогательных файлов, в том числе <a href="https://gitlab.com/lockie/otus-video-player/-/blob/master/meson.build" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><code>meson.build</code><span class="wpel-icon wpel-image wpel-icon-6"></span></a>. Это файл с инструкциями для системы сборки <a href="https://mesonbuild.com" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">Meson<span class="wpel-icon wpel-image wpel-icon-6"></span></a>, написанной на Python и функционально похожую на более распространённую систему сборки <a href="https://cmake.org" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">CMake<span class="wpel-icon wpel-image wpel-icon-6"></span></a>. Мы могли бы ограничиться классическим Makefile, но стартовый проект из официального руководства включает в себя сборку через Meson, а нам это только сыграет на руку, так как наш плагин будет зависеть от большого количества библиотек, и зависимости от этих библиотек будет проще прописать с помощью продвинутой системы сборки навроде Meson, нежели с помощью классической утилиты make.</p>
<p>Файл <code>meson.build</code> содержит на некотором псевдо-языке (DSL, Domain Specific Language) описание того, каким образом нужно собирать наш плагин. Meson принимает на вход это описание и генерирует входные файлы для более примитивной и низкоуровневой утилиты для сборки <a href="https://ninja-build.org" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">Ninja<span class="wpel-icon wpel-image wpel-icon-6"></span></a>, которая и будет непосредственно заниматься сборкой. Ninja была разработана как альтернатива классическому make с повышенной скоростью работы.</p>
<p>Среди библиотек, от которых зависит наш плагин, понятное дело, будет GStreamer, последней версии 1.x:</p>
<pre class="wp-block-code"><code>gst_dep = dependency('gstreamer-1.0', version : '>=1.0',
required : true, fallback : ['gstreamer', 'gst_dep'])</code></pre>
<p>Также плагин будет зависеть от <a href="https://curl.se/libcurl" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">libcurl<span class="wpel-icon wpel-image wpel-icon-6"></span></a>, поскольку как именно с помощью этой библиотеки мы будем качать из сети те байтики, которые представляют собой видео с Vimeo:</p>
<pre class="wp-block-code"><code>curl_dep = dependency('libcurl', version : '>= 7.66.0', required : true)</code></pre>
<figure class="wp-block-image"><img decoding="async" src="https://ip-calculator.ru/blog/wp-content/uploads/2019/03/good_curl_logo-1200x459.png" alt="ООП на C: пишем видеоплеер"/></figure>
<p>Мы используем <a href="http://xmlsoft.org" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">libxml2<span class="wpel-icon wpel-image wpel-icon-6"></span></a> для парсинга той HTML-странички, которую нам будет отдавать Vimeo:</p>
<pre class="wp-block-code"><code>libxml2_dep = dependency('libxml2', version : '>= 2.9.3', required : true)</code></pre>
<figure class="wp-block-image"><img decoding="async" src="https://dashboard.snapcraft.io/site_media/appmedia/2018/08/icon_3u2Oxjd.png" alt="ООП на C: пишем видеоплеер"/></figure>
<p>Кроме того, мы используем библиотеку <a href="https://github.com/kgabis/parson" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">Parson<span class="wpel-icon wpel-image wpel-icon-6"></span></a> для разбора JSON, но эта библиотека подключена в виде своих исходников непосредственно в нашем репозитории, в каталоге contrib (от англ. contributed), через <code>git submodule</code>, поэтому мы попросту включаем единственный исходный файл этой библиотеки в список исходных файлов нашего плагина:</p>
<pre class="wp-block-code"><code>plugin_sources = [
'src/vimeosource.c',
'src/config.c',
'src/http.c',
'contrib/parson/parson.c'
]</code></pre>
<p>И, наконец, финальным аккордом собираем всю информацию о сборке плагина в одном месте:</p>
<pre class="wp-block-code"><code>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,
)</code></pre>
<p>Для запуска нашего плагина в репозитории есть шелл-скрипт <a href="https://gitlab.com/lockie/otus-video-player/-/blob/master/launch.sh" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><code>launch.sh</code><span class="wpel-icon wpel-image wpel-icon-6"></span></a>. Первым делом в нём определяется директория, в которой будет находиться собранный плагин:</p>
<pre class="wp-block-code"><code>plugin_path=$(realpath "$(dirname "$0")")/bin</code></pre>
<p>Далее используется утилита, которая входит в базовую поставку самого GStreamer и называется <a href="https://gstreamer.freedesktop.org/documentation/tools/gst-launch.html" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><code>gst-launch</code><span class="wpel-icon wpel-image wpel-icon-6"></span></a>. Предназначение этой утилиты состоит в том, чтобы запускать произвольные контейнеры, переданные ей в текстовом виде:</p>
<pre class="wp-block-code"><code>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</code></pre>
<p>В скрипте мы указываем каталог, в котором <code>gst-launch</code> надлежит искать дополнительные плагины, через аргумент командной строки <code>--gst-plugin-path</code>. Кроме того, мы включаем чуть более болтливые логи с помощью аргументов <code>-v</code> и <code>-m</code>. Затем мы передаём конвейер, который хотим запустить.</p>
<p>Первый элемент в конвейере — тот самый, который мы создаём; мы назвали его <code>vimeosource</code>. У данного элемента есть свойство <code>location</code>, в которое мы и передаём ссылку на страничку с видео. Восклицательный знак здесь — это аналог символа <code>|</code> в шелл-скриптах, он просто указывает, что мы соединяем два элемента друг с другом с помощью их контактных гнёзд. Далее в конвейере мы используем стандартный элемент <a href="https://gstreamer.freedesktop.org/documentation/playback/decodebin.html?gi-language=c" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><code>decodebin</code><span class="wpel-icon wpel-image wpel-icon-6"></span></a>, который умеет автоматически у себя под капотом создавать правильный элемент для демультиплексирования данных, которые в него приходят. По сути, <code>decodebin</code> — контейнерный элемент, его задача — содержать другие элементы и <em>автоматически</em> разбираться с тем, какого типа элементы требуется создавать. В нашем случае <code>decodebin</code>, скорее всего, создаст под капотом элемент <a href="https://gstreamer.freedesktop.org/documentation/videoparsersbad/h264parse.html?gi-language=c" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><code>h264parse</code><span class="wpel-icon wpel-image wpel-icon-6"></span></a>, так как видео от Vimeo приходит в качестве H.264 потока.</p>
<p>Итак, мы будем передавать байты, полученные по сети, в элемент <code>decodebin</code>, который не занимается декодированием, а просто демультиплексирует входной поток данных. Впрочем, если бы мы соединили <code>decodebin</code> с элементом типа <code>autovideosink</code>, который воспроизводит видео в GUI-окне (то есть если бы мы пропустили промежуточные шаги), такой вариант по-прежнему работал бы, так как <code>decodebin</code> «понял» бы, что от него ожидают уже готовое к воспроизведению несжатое видео, и создал бы внутри себя дополнительные элементы для декодирования (скорее всего, <a href="https://gstreamer.freedesktop.org/documentation/libav/avdec_h264.html?gi-language=c" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><code>avdec_h264</code><span class="wpel-icon wpel-image wpel-icon-6"></span></a>).</p>
<p>В нашем случае мы поступаем чуть хитрее: задаём имя <code>dmux</code> элементу <code>decodebin</code>, и на этом данная часть конвейера заканчивается. Затем мы обращаемся к этому элементу по имени (<code>dmux.</code>) и соединяем его выход сразу с двумя элементами:</p>
<ol><li>Первое контактное гнездо соединяется с элементом <a href="https://gstreamer.freedesktop.org/documentation/videoparsersbad/h264parse.html?gi-language=c" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><code>queue</code><span class="wpel-icon wpel-image wpel-icon-6"></span></a>, который занимается буферизацией, и соединяется затем с элементом <a href="https://gstreamer.freedesktop.org/documentation/audioconvert/index.html?gi-language=c" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><code>audioconvert</code><span class="wpel-icon wpel-image wpel-icon-6"></span></a>, который опять-таки автомагически декодирует аудио (учитывая специфику Vimeo, скорее всего с использованием кодека AAC) в обычный сырой звук, готовый для воспроизведения, которым, в свою очередь, занимается элемент <a href="https://gstreamer.freedesktop.org/documentation/autodetect/autoaudiosink.html?gi-language=c" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><code>autoaudiosink</code><span class="wpel-icon wpel-image wpel-icon-6"></span></a>.</li><li>Второе контактное гнездо <code>decodebin</code> тоже сначала соединяется с <code>queue</code> для буферизациии данных, затем с элементом <a href="https://gstreamer.freedesktop.org/documentation/autoconvert/autovideoconvert.html?gi-language=c" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><code>autovideoconvert</code><span class="wpel-icon wpel-image wpel-icon-6"></span></a>, который декодирует видео и передает готовое для воспроизведения видео в <a href="https://gstreamer.freedesktop.org/documentation/autodetect/autovideosink.html?gi-language=c" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><code>autovideosink</code><span class="wpel-icon wpel-image wpel-icon-6"></span></a>.</li></ol>
<p>Такой усложнённый конвейер с двумя элементами <code>queue</code> в нашем случае необходим для синхронизации аудиодорожки с видео.</p>
<p>Далее рассмотрим самый главный файл с исходным кодом <a href="https://gitlab.com/lockie/otus-video-player/-/blob/master/src/vimeosource.c" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><code>src/vimeosource.c</code><span class="wpel-icon wpel-image wpel-icon-6"></span></a> и соответствующий ему заголовочный файл <a href="https://gitlab.com/lockie/otus-video-player/-/blob/master/src/vimeosource.h" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><code>src/vimeosource.h</code><span class="wpel-icon wpel-image wpel-icon-6"></span></a>.</p>
<p>В <code>src/vimeosource.h</code> мы наследуемся от вышеупомянутого <code>GstPushSrc</code>. Структура, которая будет соответствовать объекту нашего элемента <code>vimeosource</code> — <code>_VimeoSource</code>; каждый раз, когда в конвейере будет создаваться данный элемент, фреймворк будет создавать данную структуру.</p>
<pre class="wp-block-code"><code>struct _VimeoSource
{
GstPushSrc base_vimeosource;
gchar* location;
gchar* file_location;
CURLM* curlm;
CURL* curl;
GstBuffer* current_buffer;
};</code></pre>
<p>В этой структуре у нас снова используется вышеописанный трюк: первым полем идёт структура <code>GstPushSrc</code>, а за ним — поля с некоторой информацией, которая необходима для работы нашего класса. На самом деле, если от класса планируется в дальнейшем наследоваться, то так делать не рекомендуется, вместо этого принято использовать специальный механизм под названием <em>private data</em>. Однако у нас дальнейшего наследования не предполагается, поэтому мы просто сваливаем все нужные поля в нашу структуру.</p>
<p>Далее мы определяем структуру, соответствующую классу — она будет создаваться лишь в единственном экземпляре на всё приложение:</p>
<pre class="wp-block-code"><code>struct _VimeoSourceClass
{
GstPushSrcClass base_vimeosource_class;
};</code></pre>
<p>В файле <code>src/vimeosource.c</code> располагается бизнес-логика. Первая функция, определяемая в файле — <code>_vimeosource_class_init</code>. Эта функция будет вызываться GLib единожды для инициализации класса нашего элемента. Тут можно усмотреть параллель с так называемыми <a href="https://docs.python.org/3/reference/datamodel.html#metaclasses" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><em>метаклассами</em><span class="wpel-icon wpel-image wpel-icon-6"></span></a> в языке Python, которые управляют созданием других классов. Объявление функции <code>_vimeosource_class_init</code> происходит автоматически с помощью макроса <code>G_DEFINE_TYPE_WITH_CODE</code>, который вызывается чуть выше:</p>
<pre class="wp-block-code"><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"));</code></pre>
<p>Данный макрос генерирует для нас довольно много кода, и, среди всего прочего, объявляет ряд функций, среди которых — функция инициализации класса.</p>
<p>Самое важное, что мы делаем в функции инициализации класса, — это установка в структуре класса указателей на функции, которые соответствуют переопределённым в нашем классе виртуальным методам:</p>
<pre class="wp-block-code"><code>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;
/* ... */</code></pre>
<p>Прежде всего, мы переопределяем ряд виртуальных методов <a href="https://developer.gnome.org/gobject/stable/gobject-The-Base-Object-Type.html#GObjectClass" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">самого <code>GObject</code><span class="wpel-icon wpel-image wpel-icon-6"></span></a>, а именно — <code>set_property</code>, <code>get_property</code> и <code>finalize</code>. Последний занимается финализацией экземпляров класса, то есть примерно соответствует деструкторам C++, а <code>set_property</code> и <code>get_property</code> нам нужны для поддержки кастомного свойства <code>location</code> — того самого, которое мы задаём нашему элементу в конвейере. Механизм свойств не специфичен для GStreamer, он <a href="https://developer.gnome.org/gobject/stable/gobject-properties.html" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">входит<span class="wpel-icon wpel-image wpel-icon-6"></span></a> в GLib-овскую ООП-машинерию.</p>
<p>Далее мы переопределяем ряд виртуальных методов, определённых в предках нашего класса — <code>GstElement</code>, <code>GstBaseSrc</code> и <code>GstPushSrc</code>:</p>
<pre class="wp-block-code"><code> 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);</code></pre>
<p>Метод <a href="https://gstreamer.freedesktop.org/documentation/base/gstbasesrc.html?gi-language=c#GstBaseSrcClass::negotiate" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><code>negotiate</code><span class="wpel-icon wpel-image wpel-icon-6"></span></a> является частью вышеописанного механизма caps negotiation — он сообщает фреймворку о том, подходят ли элементу заданные ему типы выходных данных. Наша реализация этого метода тривиальна — мы всегда возвращаем значение <code>TRUE</code>:</p>
<pre class="wp-block-code"><code>static gboolean _vimeosource_negotiate(GstBaseSrc* src)
{
VimeoSource* vimeosource = _VIMEOSOURCE(src);
GST_DEBUG_OBJECT(vimeosource, "negotiate");
return TRUE;
}</code></pre>
<p>то есть соглашаемся со всеми типами, которые от нас ожидает фреймворк (множество таких типов в любом случае ограничивается <code>video/x-h264</code>, указанным нами выше при создании контактного гнезда с помощью макроса <code>GST_STATIC_PAD_TEMPLATE</code>). Без переопределения этого метода, к сожалению, элемент не заработает, так как caps negotiation для него будет завершаться неудачей.</p>
<p>Метод <a href="https://gstreamer.freedesktop.org/documentation/base/gstbasesrc.html?gi-language=c#GstBaseSrcClass::query" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><code>query</code><span class="wpel-icon wpel-image wpel-icon-6"></span></a> вызывается фреймворком для запрашивания некоторой мета-информации о текущем состоянии нашего элемента. В его реализации мы сначала делаем отладочный вывод — добавляем в журнал сообщение о том, что этот метод был вызван с определённым запросом:</p>
<pre class="wp-block-code"><code>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)));</code></pre>
<p>Далее, по сути, все запросы о состоянии элемента мы перенаправляем родительским классам:</p>
<pre class="wp-block-code"><code> if(!ret)
ret = GST_BASE_SRC_CLASS(_vimeosource_parent_class)->query(src, query);
return ret;</code></pre>
<p>Макрос <code>GST_BASE_SRC_CLASS</code> возвращает базовый класс, мы обращаемся через <code>-></code> к полю его структуры, которое будет указателем на функцию-метод, и, наконец, вызываем эту функцию с теми аргументами, которые мы получили от фреймворка.</p>
<p>Однако запрос типа <code>GST_QUERY_URI</code> мы обрабатываем самостоятельно:</p>
<pre class="wp-block-code"><code> switch(GST_QUERY_TYPE(query))
{
case GST_QUERY_URI:
gst_query_set_uri(query, vimeosource->location);
ret = TRUE;
break;</code></pre>
<p>В ответ на такой запрос мы возвращаем текущий <code>location</code>, выставленный для нашего элемента. Здесь <code>vimeosource</code> — это экземпляр структуры <code>_VimeoSource</code>, и для получения необходимой информации мы попросту обращаемся к её полю <code>location</code>. Затем мы сохраняем полученную строку с помощью вызова функции <a href="https://developer.gnome.org/gstreamer/stable/gstreamer-GstQuery.html#gst-query-set-uri" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><code>gst_query_set_uri</code><span class="wpel-icon wpel-image wpel-icon-6"></span></a> в полученный аргументом объект типа <a href="https://gstreamer.freedesktop.org/documentation/gstreamer/gstquery.html" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><code>GstQuery</code><span class="wpel-icon wpel-image wpel-icon-6"></span></a>, который является многофункциональным объектом, способным также хранить произвольную информацию, полученную в качестве ответа на запрос от фреймворка.</p>
<p>Мы могли бы и не переопределять метод <code>query</code>, но всё-таки делаем это из следующих соображений. Дело в том, что в GStreamer есть элементы, аналогичные вышерассмотренному <code>decodebin</code>, которые автоматически создают дочерние элементы, и подобный запрос URL (точнее, URI — Universal Resource Identifier) может быть отправлен такими элементами для определения того, какого типа дочерний элемент должен быть создан для заданного этим элементам URI. Мы не планируем пользоваться этим механизмом, но для порядка реализуем 👌</p>
<p>Методы <a href="https://gstreamer.freedesktop.org/documentation/base/gstbasesrc.html?gi-language=c#GstBaseSrcClass::start" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><code>start</code><span class="wpel-icon wpel-image wpel-icon-6"></span></a> и <a href="https://gstreamer.freedesktop.org/documentation/base/gstbasesrc.html?gi-language=c#GstBaseSrcClass::stop" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><code>stop</code><span class="wpel-icon wpel-image wpel-icon-6"></span></a> отвечают за начало и конец работы нашего элемента в рамках конвейра. В <code>start</code> мы должны инициализировать ресурсы, необходимые для работы, а в <code>stop</code> — финализировать их. В нашей реализации метода <code>start</code> довольно много кода, но он достаточно простой, как и практически любой код на C 😊</p>
<p>Прежде всего, мы записываем в журнал тот факт, что метод был вызван:</p>
<pre class="wp-block-code"><code>static gboolean _vimeosource_start(GstBaseSrc* src)
{
VimeoSource* vimeosource = _VIMEOSOURCE(src);
GST_DEBUG_OBJECT(vimeosource, "start");</code></pre>
<p>Затем получаем непосредственный URL видеофайла по URL страницы на Vimeo, который нам передали в свойстве <code>location</code>. Для этого мы используем код из предыдущего открытого урока, а именно — функцию <code>get_file_url</code>, объявленную в <a href="https://gitlab.com/lockie/otus-video-player/-/blob/master/src/config.h" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><code>src/config.h</code><span class="wpel-icon wpel-image wpel-icon-6"></span></a> и определённую в <a href="https://gitlab.com/lockie/otus-video-player/-/blob/master/src/config.c" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><code>src/config.c</code><span class="wpel-icon wpel-image wpel-icon-6"></span></a>. Функция работает с libcurl для загрузки страницы видео, ищет на странице JSON-объект, в котором прописан специальный <em>config URL</em>, затем обращается на этот URL, получает в ответ ещё порцию JSON’а, и уже из него получает ссылки на медиафайлы. После чего функция запускает цикл, который проходится по всем медиафайлам и ищет тот, у которого наибольшее разрешение, а найдя, возвращает его. В <a href="http://youtu.be/46u8KAILwew" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">предыдущем открытом уроке<span class="wpel-icon wpel-image wpel-icon-6"></span></a> мы подробнее рассматривали все эти механизмы.</p>
<p>Мы записываем полученный URL видеофайла в поле <code>file_location</code> нашей структуры <code>VimeoSource</code>:</p>
<pre class="wp-block-code"><code> setlocale(LC_NUMERIC, "C"); // see https://git.io/Jte2C
vimeosource->file_location = get_file_url(vimeosource->location);
setlocale(LC_NUMERIC, "");</code></pre>
<p>Здесь сразу виден большой подводный камень, про который я сам знал, но забыл и при подготовке этого кода угробил почти целый день, пытаясь понять, что же пошло не так. Суть проблемы в том, что библиотека для разбора JSON Parson разбирает числовые поля с помощью функции <a href="https://linux.die.net/man/3/strtod" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><code>strtod</code><span class="wpel-icon wpel-image wpel-icon-6"></span></a>, которая конвертирует строки в числа с плавающей запятой, но эта функция зависит от текущей локали. В русскоязычной локали разделителем десятичных разрядов является запятая, а не точка, как это принято в стандартной английской локали. Таким образом, при запуске кода с русскоязычной локалью запятая, которая идёт после поля, считается частью числа, и поэтому парсинг заканчивается неудачей. Что интересно, автору библиотеки неоднократно <a href="https://github.com/kgabis/parson/issues/142" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">писали<span class="wpel-icon wpel-image wpel-icon-6"></span></a> про эту проблему в багтрекер, но он эту особенность исправлять отказался, мол, локали не во власти моей библиотеки. Чтобы исправить эту проблему, на время получения URL на видеофайл через <code>get_file_url</code>, у которой под капотом и происходит разбор JSON через Parson, мы временно переключаем аспект <code>LC_NUMERIC</code> локали, отвечающий за форматирование чисел, на стандартную, так называемую C locale.</p>
<p>Далее мы журналируем полученный на предыдущем шаге URL видеофайла:</p>
<pre class="wp-block-code"><code> GST_DEBUG_OBJECT(vimeosource, "location=%s", vimeosource->file_location);</code></pre>
<p>Затем мы производим манипуляции, связанные с инициализацией libcurl:</p>
<pre class="wp-block-code"><code> vimeosource->curlm = curl_multi_init();
g_assert(vimeosource->curlm);
if(!vimeosource->curlm)
return GST_FLOW_ERROR;</code></pre>
<p>В libcurl, среди всего прочего, есть механизм под названием <a href="https://curl.se/libcurl/c/libcurl-multi.html" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">multi interface<span class="wpel-icon wpel-image wpel-icon-6"></span></a>, который позволяет скомбинировать несколько закачек в одну в асинхронном режиме; предполагается, что он будет использоваться вместе с каким-либо механизмом мультиплескирования ввода-вывода вроде <a href="https://linux.die.net/man/2/select" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><code>select</code><span class="wpel-icon wpel-image wpel-icon-6"></span></a> или <a href="https://linux.die.net/man/2/poll" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><code>poll</code><span class="wpel-icon wpel-image wpel-icon-6"></span></a>, либо со своей родной функцией для ожидания <a href="https://curl.se/libcurl/c/curl_multi_poll.html" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><code>curl_multi_poll</code><span class="wpel-icon wpel-image wpel-icon-6"></span></a>. Мы будем использовать этот механизм по следующим соображениям. Каждый раз, когда фреймворк будет вызывать метод <code>create</code> у нашего элемента, он должен будет возвращать очередной буфер с накопившимися на данный момент данными, такова суть работы источника в push-режиме. Если бы мы использовали простой <a href="https://curl.se/libcurl/c/libcurl-easy.html" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">curl easy interface<span class="wpel-icon wpel-image wpel-icon-6"></span></a>, у нас бы не получилось выстроить такую схему, т.к. при вызове <a href="https://curl.se/libcurl/c/curl_easy_perform.html" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><code>curl_easy_perform</code><span class="wpel-icon wpel-image wpel-icon-6"></span></a> управление не возвращается из этой функции до тех пор, пока файл не будет скачан до конца; нас это не удовлетворяет, т.к. нам нужно скачать файл не за раз целиком, а скармливать фреймворку маленькими кусочками.</p>
<p>Мы создаём обычный curl easy handle, устанавливаем для него все нужные нам параметры с помощью функции <a href="https://curl.se/libcurl/c/curl_easy_setopt.html" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><code>curl_easy_setopt</code><span class="wpel-icon wpel-image wpel-icon-6"></span></a> и добавляем его в multi handle через функцию <a href="https://curl.se/libcurl/c/curl_multi_add_handle.html" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><code>curl_multi_add_handle</code><span class="wpel-icon wpel-image wpel-icon-6"></span></a>:</p>
<pre class="wp-block-code"><code> 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;</code></pre>
<p>В качестве callback’а (функции обратного вызова) для easy handle мы выставляем нашу функцию <code>curl_callback</code>, которая по сути создаёт те буферы с данными, которые от нас ожидает фреймворк, и возвращает их. Через аргумент callback’а <code>WRITEDATA</code> мы передаём сам экземпляр нашего класса <code>VimeoSource</code>. В нём у нас есть поле <code>current_buffer</code>, в которое в callback’е мы будем прост класть очередной буфер, который удалось считать из сети.</p>
<p>Далее мы вызываем функцию <a href="https://curl.se/libcurl/c/curl_multi_perform.html" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><code>curl_multi_perform</code><span class="wpel-icon wpel-image wpel-icon-6"></span></a>, которая не блокирует управление, а возвращает его после начала работы переданного ей multi handle:</p>
<pre class="wp-block-code"><code> int dummy;
ret = curl_multi_perform(vimeosource->curlm, &dummy);
g_assert(ret == CURLM_OK);
if(ret != CURLM_OK)
return GST_FLOW_ERROR;
return TRUE;
}</code></pre>
<p>Метод <code>stop</code> — злой брат-близнец метода <code>start</code>, он просто уничтожает в обратном созданию порядке все ресурсы, инициализированные в <code>start</code>. Мы уничтожаем созданный multi handle, если он есть:</p>
<pre class="wp-block-code"><code>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;
}</code></pre>
<p>так же поступаем с easy handle:</p>
<pre class="wp-block-code"><code> if(vimeosource->curl)
{
curl_easy_cleanup(vimeosource->curl);
vimeosource->curl = NULL;
}</code></pre>
<p>и освобождаем память, отведённую под строки, хранившие URL страницы на Vimeo и URL видеофайла:</p>
<pre class="wp-block-code"><code> g_free(vimeosource->file_location);
vimeosource->file_location = NULL;
g_free(vimeosource->location);
vimeosource->location = NULL;
return TRUE;
}</code></pre>
<p>Наконец, сердце нашего элемента — метод <a href="https://gstreamer.freedesktop.org/documentation/base/gstbasesrc.html?gi-language=c#GstBaseSrcClass::create" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><code>create</code><span class="wpel-icon wpel-image wpel-icon-6"></span></a>. Вопреки названию, он не связан с созданием элемента; напротив, он вызывается фреймворком в тот момент, когда GStreamer от нас нужен очередной буфер с данными. Под названием <code>create</code> подразумевается, что мы создаём этот самый буфер. GStreamer передаёт нам аргументом двойной указатель на этот буфер <code>buf</code>, изначально указывающий на нулевой указатель, и мы по этому указателю должны положить вместо <code>NULL</code> новосозданный буфер, содержащий очередной фрагмент данных. Мы делаем это под конец функции — просто кладём туда то самое поле <code>current_buffer</code> из структуры нашего элемента и возвращаем значение <code>GST_FLOW_OK</code>, сигнализирующее о том, что мы удачно создали буфер:</p>
<pre class="wp-block-code"><code>static GstFlowReturn _vimeosource_create(GstBaseSrc* src, guint64 offset,
guint size, GstBuffer** buf)
{
VimeoSource* vimeosource = _VIMEOSOURCE(src);
/* ... */
*buf = vimeosource->current_buffer;
return GST_FLOW_OK;
}</code></pre>
<p>Вся соль нашей реализации в том, что мы в цикле вызываем функцию <a href="https://curl.se/libcurl/c/curl_multi_poll.html" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><code>curl_multi_poll</code><span class="wpel-icon wpel-image wpel-icon-6"></span></a>, которая похожа на обычный системный вызов <a href="https://linux.die.net/man/2/poll" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><code>poll</code><span class="wpel-icon wpel-image wpel-icon-6"></span></a> — она постоянно опрашивает (англ. <em>to poll</em>) дескрипторы, которые есть под капотом у переданного ей curl multi handle до тех пор, пока на каком-то из этих дескрипторов не появятся новые данные. Как только данные появляются, мы на этой же итерации цикла вызываем функцию <a href="https://curl.se/libcurl/c/curl_multi_perform.html" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><code>curl_multi_perform</code><span class="wpel-icon wpel-image wpel-icon-6"></span></a>. Последняя функция довольно хитрая: она может вызвать наш callback с прочитанными данными, а может в ряде случаев (например, при обработке HTTP-заголовков) и не вызвать, и при этом вернуть значение <code>CURLM_OK</code>, сигнализирующее о том, что всё прошло успешно. Именно поэтому мы в цикле while, пока не получим из callback’а новый буфер, вызываем эти функции: сначала с помощью <code>curl_mutli_poll</code> ждём появления новых данных, затем через <code>curl_mutli_perform</code> обрабатываем эти новые данные с помощью callback’а, и, когда в результате обработки мы получаем непустой <code>current_buffer</code>, мы его и возвращаем:</p>
<pre class="wp-block-code"><code> 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;
}</code></pre>
<p>Теперь выходит на сцену вышеупомянутый механизм подсчёта ссылок из GLib. В callback’е мы выделяем кусок памяти под сами данные:</p>
<pre class="wp-block-code"><code>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);</code></pre>
<p>затем создаём регион памяти <a href="https://gstreamer.freedesktop.org/documentation/gstreamer/gstmemory.html" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><code>GstMemory</code><span class="wpel-icon wpel-image wpel-icon-6"></span></a>, являющийся тонкой обёрткой над этим куском памяти, через функцию <a href="https://gstreamer.freedesktop.org/documentation/gstreamer/gstmemory.html?gi-language=c#gst_memory_new_wrapped" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><code>gst_memory_new_wrapped</code><span class="wpel-icon wpel-image wpel-icon-6"></span></a>; последним аргументом эта функция принимает указатель на функцию, которая будет вызвана для удаления этого региона памяти: мы попросту передаём в качестве такой функции <a href="https://developer.gnome.org/glib/stable/glib-Memory-Allocation.html#g-free" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><code>g_free</code><span class="wpel-icon wpel-image wpel-icon-6"></span></a>, которая соответствует предыдущему вызову <a href="https://developer.gnome.org/glib/stable/glib-Memory-Allocation.html#g-malloc" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><code>g_malloc</code><span class="wpel-icon wpel-image wpel-icon-6"></span></a>:</p>
<pre class="wp-block-code"><code> GstMemory* memory
= gst_memory_new_wrapped(0, data, realsize, 0, realsize, data, g_free);
g_assert(memory);
if(!memory)
return 0;</code></pre>
<p>Далее, в начале функции мы создаём новый буфер в <code>current_buffer</code> через вызов <a href="https://gstreamer.freedesktop.org/documentation/gstreamer/gstbuffer.html?gi-language=c#gst_buffer_new" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><code>gst_buffer_new</code><span class="wpel-icon wpel-image wpel-icon-6"></span></a>, а под конец добавляем в буфер наш регион памяти:</p>
<pre class="wp-block-code"><code> vimeosource->current_buffer = gst_buffer_new();
/* ... */
gst_buffer_insert_memory(vimeosource->current_buffer, -1, memory);
return realsize;
}</code></pre>
<p>Возвращаясь к подсчёту ссылок, мы создали буфер, динамический объект, через функцию <code>gst_buffer_new</code>, но при этом мы нигде в нашем коде не вызываем функцию для его удаления. Несмотря на это, у нас не будет происходить утечка памяти (я проверял 😂). Не происходит она потому, что буфер создаётся со счётчиком ссылок, равным 1. Мы его отправляем во фреймворк через аргумент <code>buf</code> в методе <code>create</code>, фреймворк этот буфер нужным ему образом обрабатывает, и, когда буфер становится ему ненужным, уменьшает счётчик ссылок на 1, в результате чего он становится равным нулю, и внутренняя машинерия GLib автоматически удаляет этот динамический объект.</p>
<p>Далее, у нас есть метод <code>finalize</code>, в котором мы, помимо журналирования и передачи управления в метод <code>finalize</code> родительского класса, вызваем функцию <a href="https://curl.se/libcurl/c/curl_global_cleanup.html" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><code>curl_global_cleanup</code><span class="wpel-icon wpel-image wpel-icon-6"></span></a>, которая корректным образом финализирует некое глобальное внутреннее состояние библиотеки libcurl.</p>
<p>Наконец, для того, чтобы запустить наш код, нужно в корневом каталоге проекта создать каталог с именем <code>bin</code>, затем в этом каталоге вызвать команду <code>meson ..</code>, чтобы Meson нам сгенерировал файлы для сборки через Ninja (а именно, файл <code>build.ninja</code>):</p>
<pre class="wp-block-code"><code>$ 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</code></pre>
<p>При этом Meson сообщит нам инфорацию о собственной версии, версии компилятора и о версиях запрошенных нами через файл <code>meson.build</code> библиотек. Далее, для непосредственной сборки нашего плагина, мы просто без аргументов вызываем <code>ninja</code>.<br>Результат сборки — файл <code>libvimeosource.so</code>, это разделяемая библиотека (SO — shared object). Мы можем заглянуть ей под капот и посмотреть на функции, которые экспортируются и импортируются в неё, с помощью команды <code>nm -D libvimeosource.so</code>:</p>
<pre class="wp-block-code"><code>$ 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</code></pre>
<p>Мы можем увидеть внутренние функции для разбора JSON, которые мы использовали — у них префикс <code>json_</code>, а также импорты стандартных библиотечных функций, как то: <code>malloc</code>, <code>memcmp</code> и так далее, а также импорты функций из библиотеки libcurl и из самого GStreamer.</p>
<p>Так как это библиотека, запустить напрямую мы её не можем, но зато у нас есть скрипт для запуска <code>launch.sh</code>, который умеет её подгружать в конвейер GStreamer. Мы можем убедиться, что GStreamer подргужает именно наш код следующим образом:</p>
<pre class="wp-block-code"><code>$ rm libvimeosource.so
$ ../launch.sh
ПРЕДУПРЕЖДЕНИЕ: ошибочный конвейер: элемент «vimeosource» не найден</code></pre>
<p>Почему элемент не найден? Да потому что я его удалил.</p>
<p>Скомпилируем библиотеку заново через вызов <code>ninja</code> и запустим <code>../launch.sh</code>. В качестве примера видео для воспроизведения в скрипте прописана ссылка на мультфильм под названием <a href="https://www.kinopoisk.ru/film/566766" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">Sintel<span class="wpel-icon wpel-image wpel-icon-6"></span></a>, и мы можем убедиться, что он действительно воспроизводится и даже проигрывает звук 🎉</p>
<figure class="wp-block-image"><img decoding="async" src="https://i.imgur.com/b05rOw5.png" alt="ООП на C: пишем видеоплеер"/></figure>
<p>Sintel — коротенький мультфильм с душераздирающим сюжетом, созданный для рекламы опенсорсного пакета 3D-моделирования <a href="https://blender.org" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">Blender<span class="wpel-icon wpel-image wpel-icon-6"></span></a>, который, кстати, тоже написан на чистом C.</p>
<p>Подводя итог, мы написали библиотеку, которая содержит внутри себя элемент конвейра GStreamer, и мы успешно использовали этот элемент для воспроизведения видео с Vimeo.</p>
<p>Важная особенность состоит в том, что наша библиотека универсальна, мы можем использовать написанный нами элемент в произвольных конвейерах, в том числе теоретически можно взять произвольное приложение, использующее GStreamer, например, какой-нибудь третьесторонний плеер, и заставить его использовать наш элемент и воспроизводить видео с Vimeo. Таким образом, мы на конкретном примере наблюдаем гибкость данного фреймворка.</p>
</div><!-- .post-content -->
<div class="the-post-foot cf">
<div class="tag-share cf">
<div class="post-tags"><a href="https://otus.ru/journal/tag/programmirovanie/" rel="tag" data-wpel-link="internal">программирование</a><a href="https://otus.ru/journal/tag/urok/" rel="tag" data-wpel-link="internal">урок</a></div>
<div class="post-share">
<div class="post-share-icons cf">
<span class="counters">
</span>
<a href="https://www.facebook.com/sharer.php?u=https%3A%2F%2Fotus.ru%2Fjournal%2Foop-na-c-pishem-videopleer%2F" class="link facebook wpel-icon-right" target="_blank" title="Share on Facebook" data-wpel-link="external" rel="nofollow external noopener noreferrer"><i class="fa fa-facebook"></i><span class="wpel-icon wpel-image wpel-icon-6"></span></a>
<a href="https://twitter.com/intent/tweet?url=https%3A%2F%2Fotus.ru%2Fjournal%2Foop-na-c-pishem-videopleer%2F&text=%D0%9E%D0%9E%D0%9F%20%D0%BD%D0%B0%20C%3A%20%D0%BF%D0%B8%D1%88%D0%B5%D0%BC%20%D0%B2%D0%B8%D0%B4%D0%B5%D0%BE%D0%BF%D0%BB%D0%B5%D0%B5%D1%80" class="link twitter wpel-icon-right" target="_blank" title="Share on Twitter" data-wpel-link="external" rel="nofollow external noopener noreferrer"><i class="fa fa-twitter"></i><span class="wpel-icon wpel-image wpel-icon-6"></span></a>
<a href="https://www.linkedin.com/shareArticle?mini=true&url=https%3A%2F%2Fotus.ru%2Fjournal%2Foop-na-c-pishem-videopleer%2F" class="link linkedin wpel-icon-right" target="_blank" title="LinkedIn" data-wpel-link="external" rel="nofollow external noopener noreferrer"><i class="fa fa-linkedin"></i><span class="wpel-icon wpel-image wpel-icon-6"></span></a>
<a href="https://pinterest.com/pin/create/button/?url=https%3A%2F%2Fotus.ru%2Fjournal%2Foop-na-c-pishem-videopleer%2F&media=https%3A%2F%2Fotus.ru%2Fjournal%2Fwp-content%2Fuploads%2F2021%2F05%2Foj-1080x720-kopiya-1-1.png&description=%D0%9E%D0%9E%D0%9F%20%D0%BD%D0%B0%20C%3A%20%D0%BF%D0%B8%D1%88%D0%B5%D0%BC%20%D0%B2%D0%B8%D0%B4%D0%B5%D0%BE%D0%BF%D0%BB%D0%B5%D0%B5%D1%80" class="link pinterest wpel-icon-right" target="_blank" title="Pinterest" data-wpel-link="external" rel="nofollow external noopener noreferrer"><i class="fa fa-pinterest-p"></i><span class="wpel-icon wpel-image wpel-icon-6"></span></a>
</div>
</div>
</div>
</div>
<div class="post-nav">
<div class="post previous cf">
<a href="https://otus.ru/journal/novye-meropriyatiya-v-otus-8/" title="Prev Post" class="nav-icon" data-wpel-link="internal">
<i class="fa fa-angle-left"></i>
</a>
<span class="content">
<a href="https://otus.ru/journal/novye-meropriyatiya-v-otus-8/" class="image-link" rel="previous" data-wpel-link="internal">
<img width="150" height="100" src="data:image/svg+xml,%3Csvg%20viewBox%3D%270%200%20150%20100%27%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%3E%3C%2Fsvg%3E" class="attachment-thumbnail size-thumbnail lazyload wp-post-image" alt="Новые мероприятия в OTUS" decoding="async" data-srcset="https://otus.ru/journal/wp-content/uploads/2021/05/oj-1080x720-10-150x100.png 150w, https://otus.ru/journal/wp-content/uploads/2021/05/oj-1080x720-10-300x200.png 300w, https://otus.ru/journal/wp-content/uploads/2021/05/oj-1080x720-10-1024x683.png 1024w, https://otus.ru/journal/wp-content/uploads/2021/05/oj-1080x720-10-768x512.png 768w, https://otus.ru/journal/wp-content/uploads/2021/05/oj-1080x720-10-270x180.png 270w, https://otus.ru/journal/wp-content/uploads/2021/05/oj-1080x720-10-770x515.png 770w, https://otus.ru/journal/wp-content/uploads/2021/05/oj-1080x720-10-370x245.png 370w, https://otus.ru/journal/wp-content/uploads/2021/05/oj-1080x720-10.png 1080w" data-src="https://otus.ru/journal/wp-content/uploads/2021/05/oj-1080x720-10-150x100.png" data-sizes="(max-width: 150px) 100vw, 150px" title="Новые мероприятия в OTUS" /> </a>
<div class="post-meta">
<span class="label">Prev Post</span>
<div class="post-meta post-meta-b">
<h2 class="post-title">
<a href="https://otus.ru/journal/novye-meropriyatiya-v-otus-8/" data-wpel-link="internal">Новые мероприятия в OTUS</a>
</h2>
<div class="below">
<a href="https://otus.ru/journal/novye-meropriyatiya-v-otus-8/" class="meta-item date-link" data-wpel-link="internal"><time class="post-date" datetime="2021-05-24T11:18:14+00:00">24 мая, 2021</time></a>
<span class="meta-sep"></span>
<span class="meta-item read-time">3 Mins Read</span>
</div>
</div> </div>
</span>
</div>
<div class="post next cf">
<a href="https://otus.ru/journal/krestiki-noliki/" title="Next Post" class="nav-icon" data-wpel-link="internal">
<i class="fa fa-angle-right"></i>
</a>
<span class="content">
<a href="https://otus.ru/journal/krestiki-noliki/" class="image-link" rel="next" data-wpel-link="internal">
<img width="150" height="100" src="data:image/svg+xml,%3Csvg%20viewBox%3D%270%200%20150%20100%27%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%3E%3C%2Fsvg%3E" class="attachment-thumbnail size-thumbnail lazyload wp-post-image" alt="Крестики-нолики" decoding="async" data-srcset="https://otus.ru/journal/wp-content/uploads/2021/05/oj-1080x720-kopiya-1-2-150x100.png 150w, https://otus.ru/journal/wp-content/uploads/2021/05/oj-1080x720-kopiya-1-2-300x200.png 300w, https://otus.ru/journal/wp-content/uploads/2021/05/oj-1080x720-kopiya-1-2-1024x683.png 1024w, https://otus.ru/journal/wp-content/uploads/2021/05/oj-1080x720-kopiya-1-2-768x512.png 768w, https://otus.ru/journal/wp-content/uploads/2021/05/oj-1080x720-kopiya-1-2-270x180.png 270w, https://otus.ru/journal/wp-content/uploads/2021/05/oj-1080x720-kopiya-1-2-770x515.png 770w, https://otus.ru/journal/wp-content/uploads/2021/05/oj-1080x720-kopiya-1-2-370x245.png 370w, https://otus.ru/journal/wp-content/uploads/2021/05/oj-1080x720-kopiya-1-2.png 1080w" data-src="https://otus.ru/journal/wp-content/uploads/2021/05/oj-1080x720-kopiya-1-2-150x100.png" data-sizes="(max-width: 150px) 100vw, 150px" title="Крестики-нолики" /> </a>
<div class="post-meta">
<span class="label">Next Post</span>
<div class="post-meta post-meta-b">
<h2 class="post-title">
<a href="https://otus.ru/journal/krestiki-noliki/" data-wpel-link="internal">Крестики-нолики</a>
</h2>
<div class="below">
<a href="https://otus.ru/journal/krestiki-noliki/" class="meta-item date-link" data-wpel-link="internal"><time class="post-date" datetime="2021-05-24T15:38:29+00:00">24 мая, 2021</time></a>
<span class="meta-sep"></span>
<span class="meta-item read-time">14 Mins Read</span>
</div>
</div> </div>
</span>
</div>
</div>
<section class="related-posts grid-3">
<h4 class="section-head"><span class="title">Читать ещё</span></h4>
<div class="ts-row posts cf">
<article class="post col-4">
<a href="https://otus.ru/journal/proekt-tg-autoposter-na-nest-js/" title="Проект «TG Autoposter на Nest.JS»" class="image-link" data-wpel-link="internal">
</a>
<div class="content">
<h3 class="post-title"><a href="https://otus.ru/journal/proekt-tg-autoposter-na-nest-js/" class="post-link" data-wpel-link="internal">Проект «TG Autoposter на Nest.JS»</a></h3>
<div class="post-meta">
<time class="post-date" datetime="2025-12-23T00:44:53+00:00">23 декабря, 2025</time>
</div>
</div>
</article >
<article class="post col-4">
<a href="https://otus.ru/journal/langtrainee-razrabotka-mvp-ai-platformy-dlya-personalizirovannogo-izucheniya-yazykov/" title="LangTrainee: разработка MVP AI-платформы для персонализированного изучения языков" class="image-link" data-wpel-link="internal">
</a>
<div class="content">
<h3 class="post-title"><a href="https://otus.ru/journal/langtrainee-razrabotka-mvp-ai-platformy-dlya-personalizirovannogo-izucheniya-yazykov/" class="post-link" data-wpel-link="internal">LangTrainee: разработка MVP AI-платформы для персонализированного изучения языков</a></h3>
<div class="post-meta">
<time class="post-date" datetime="2025-11-12T04:39:47+00:00">12 ноября, 2025</time>
</div>
</div>
</article >
<article class="post col-4">
<a href="https://otus.ru/journal/novye-uroki-noyabrya-tolko-top-temy-po-programmirovaniju/" title="Новые уроки ноября: только топ-темы по программированию" class="image-link" data-wpel-link="internal">
<img width="270" height="180" src="data:image/svg+xml,%3Csvg%20viewBox%3D%270%200%20270%20180%27%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%3E%3C%2Fsvg%3E" class="image lazyload wp-post-image" alt="Новые уроки ноября: только топ-темы по программированию" title="Новые уроки ноября: только топ-темы по программированию" decoding="async" loading="lazy" data-srcset="https://otus.ru/journal/wp-content/uploads/2025/11/oj-1080x720-kopiya-2-270x180.jpg 270w, https://otus.ru/journal/wp-content/uploads/2025/11/oj-1080x720-kopiya-2-770x515.jpg 770w, https://otus.ru/journal/wp-content/uploads/2025/11/oj-1080x720-kopiya-2-370x245.jpg 370w" data-src="https://otus.ru/journal/wp-content/uploads/2025/11/oj-1080x720-kopiya-2-270x180.jpg" data-sizes="(max-width: 270px) 100vw, 270px" /> </a>
<div class="content">
<h3 class="post-title"><a href="https://otus.ru/journal/novye-uroki-noyabrya-tolko-top-temy-po-programmirovaniju/" class="post-link" data-wpel-link="internal">Новые уроки ноября: только топ-темы по программированию</a></h3>
<div class="post-meta">
<time class="post-date" datetime="2025-11-09T23:24:11+00:00">9 ноября, 2025</time>
</div>
</div>
</article >
</div>
</section>
</article> <!-- .the-post -->
</div>
<aside class="col-4 sidebar">
<div class="inner">
<ul>
<li id="search-2" class="widget widget_search"><h5 class="widget-title"><span>Поиск по блогу</span></h5>
<form method="get" class="search-form" action="https://otus.ru/journal/">
<label>
<span class="screen-reader-text">Search for:</span>
<input type="search" class="search-field" placeholder="Введите запрос и нажмите Enter" value="" name="s" title="Search for:" />
</label>
<button type="submit" class="search-submit"><i class="fa fa-search"></i></button>
</form>
</li>
<li id="tag_cloud-5" class="widget widget_tag_cloud"><h5 class="widget-title"><span>Метки</span></h5><div class="tagcloud"><a href="https://otus.ru/journal/tag/android-2/" class="tag-cloud-link tag-link-74 tag-link-position-1" style="font-size: 12.472222222222pt;" aria-label="Android (34 элемента)" data-wpel-link="internal">Android</a>
<a href="https://otus.ru/journal/tag/c-3/" class="tag-cloud-link tag-link-91 tag-link-position-2" style="font-size: 10.916666666667pt;" aria-label="C (23 элемента)" data-wpel-link="internal">C</a>
<a href="https://otus.ru/journal/tag/c-2/" class="tag-cloud-link tag-link-81 tag-link-position-3" style="font-size: 12.666666666667pt;" aria-label="C# (35 элементов)" data-wpel-link="internal">C#</a>
<a href="https://otus.ru/journal/tag/c/" class="tag-cloud-link tag-link-20 tag-link-position-4" style="font-size: 12.472222222222pt;" aria-label="c++ (34 элемента)" data-wpel-link="internal">c++</a>
<a href="https://otus.ru/journal/tag/computer-science/" class="tag-cloud-link tag-link-209 tag-link-position-5" style="font-size: 15.972222222222pt;" aria-label="computer science (78 элементов)" data-wpel-link="internal">computer science</a>
<a href="https://otus.ru/journal/tag/css/" class="tag-cloud-link tag-link-288 tag-link-position-6" style="font-size: 8.6805555555556pt;" aria-label="CSS (13 элементов)" data-wpel-link="internal">CSS</a>
<a href="https://otus.ru/journal/tag/data-science/" class="tag-cloud-link tag-link-151 tag-link-position-7" style="font-size: 8pt;" aria-label="Data Science (11 элементов)" data-wpel-link="internal">Data Science</a>
<a href="https://otus.ru/journal/tag/devops/" class="tag-cloud-link tag-link-98 tag-link-position-8" style="font-size: 10.138888888889pt;" aria-label="devops (19 элементов)" data-wpel-link="internal">devops</a>
<a href="https://otus.ru/journal/tag/docker/" class="tag-cloud-link tag-link-143 tag-link-position-9" style="font-size: 8.2916666666667pt;" aria-label="Docker (12 элементов)" data-wpel-link="internal">Docker</a>
<a href="https://otus.ru/journal/tag/gamedev/" class="tag-cloud-link tag-link-25 tag-link-position-10" style="font-size: 11.694444444444pt;" aria-label="gamedev (28 элементов)" data-wpel-link="internal">gamedev</a>
<a href="https://otus.ru/journal/tag/hr/" class="tag-cloud-link tag-link-103 tag-link-position-11" style="font-size: 8pt;" aria-label="hr (11 элементов)" data-wpel-link="internal">hr</a>
<a href="https://otus.ru/journal/tag/html/" class="tag-cloud-link tag-link-217 tag-link-position-12" style="font-size: 11.208333333333pt;" aria-label="HTML (25 элементов)" data-wpel-link="internal">HTML</a>
<a href="https://otus.ru/journal/tag/ios/" class="tag-cloud-link tag-link-101 tag-link-position-13" style="font-size: 8.9722222222222pt;" aria-label="iOS (14 элементов)" data-wpel-link="internal">iOS</a>
<a href="https://otus.ru/journal/tag/it/" class="tag-cloud-link tag-link-50 tag-link-position-14" style="font-size: 10.527777777778pt;" aria-label="IT (21 элемент)" data-wpel-link="internal">IT</a>
<a href="https://otus.ru/journal/tag/java/" class="tag-cloud-link tag-link-75 tag-link-position-15" style="font-size: 15.680555555556pt;" aria-label="Java (73 элемента)" data-wpel-link="internal">Java</a>
<a href="https://otus.ru/journal/tag/javascript/" class="tag-cloud-link tag-link-83 tag-link-position-16" style="font-size: 14.319444444444pt;" aria-label="JavaScript (53 элемента)" data-wpel-link="internal">JavaScript</a>
<a href="https://otus.ru/journal/tag/linux/" class="tag-cloud-link tag-link-141 tag-link-position-17" style="font-size: 11.888888888889pt;" aria-label="Linux (29 элементов)" data-wpel-link="internal">Linux</a>
<a href="https://otus.ru/journal/tag/machine-learning/" class="tag-cloud-link tag-link-167 tag-link-position-18" style="font-size: 8.6805555555556pt;" aria-label="Machine Learning (13 элементов)" data-wpel-link="internal">Machine Learning</a>
<a href="https://otus.ru/journal/tag/otus-book/" class="tag-cloud-link tag-link-261 tag-link-position-19" style="font-size: 9.9444444444444pt;" aria-label="otus book (18 элементов)" data-wpel-link="internal">otus book</a>
<a href="https://otus.ru/journal/tag/php/" class="tag-cloud-link tag-link-45 tag-link-position-20" style="font-size: 10.527777777778pt;" aria-label="PHP (21 элемент)" data-wpel-link="internal">PHP</a>
<a href="https://otus.ru/journal/tag/python/" class="tag-cloud-link tag-link-27 tag-link-position-21" style="font-size: 16.944444444444pt;" aria-label="Python (99 элементов)" data-wpel-link="internal">Python</a>
<a href="https://otus.ru/journal/tag/qa/" class="tag-cloud-link tag-link-155 tag-link-position-22" style="font-size: 11.402777777778pt;" aria-label="qa (26 элементов)" data-wpel-link="internal">qa</a>
<a href="https://otus.ru/journal/tag/sql/" class="tag-cloud-link tag-link-38 tag-link-position-23" style="font-size: 12.861111111111pt;" aria-label="SQL (37 элементов)" data-wpel-link="internal">SQL</a>
<a href="https://otus.ru/journal/tag/team-lead/" class="tag-cloud-link tag-link-364 tag-link-position-24" style="font-size: 9.9444444444444pt;" aria-label="team lead (18 элементов)" data-wpel-link="internal">team lead</a>
<a href="https://otus.ru/journal/tag/unity/" class="tag-cloud-link tag-link-24 tag-link-position-25" style="font-size: 8pt;" aria-label="unity (11 элементов)" data-wpel-link="internal">unity</a>
<a href="https://otus.ru/journal/tag/algoritmy/" class="tag-cloud-link tag-link-30 tag-link-position-26" style="font-size: 9.9444444444444pt;" aria-label="Алгоритмы (18 элементов)" data-wpel-link="internal">Алгоритмы</a>
<a href="https://otus.ru/journal/tag/bazy-dannyh/" class="tag-cloud-link tag-link-40 tag-link-position-27" style="font-size: 10.138888888889pt;" aria-label="Базы данных (19 элементов)" data-wpel-link="internal">Базы данных</a>
<a href="https://otus.ru/journal/tag/matematika/" class="tag-cloud-link tag-link-44 tag-link-position-28" style="font-size: 10.916666666667pt;" aria-label="Математика (23 элемента)" data-wpel-link="internal">Математика</a>
<a href="https://otus.ru/journal/tag/arhitektura-po/" class="tag-cloud-link tag-link-10 tag-link-position-29" style="font-size: 9.4583333333333pt;" aria-label="архитектура ПО (16 элементов)" data-wpel-link="internal">архитектура ПО</a>
<a href="https://otus.ru/journal/tag/bazy-dannyh-2/" class="tag-cloud-link tag-link-251 tag-link-position-30" style="font-size: 10.138888888889pt;" aria-label="базы данных (19 элементов)" data-wpel-link="internal">базы данных</a>
<a href="https://otus.ru/journal/tag/vebinar/" class="tag-cloud-link tag-link-201 tag-link-position-31" style="font-size: 13.930555555556pt;" aria-label="вебинар (48 элементов)" data-wpel-link="internal">вебинар</a>
<a href="https://otus.ru/journal/tag/dajdzhest/" class="tag-cloud-link tag-link-308 tag-link-position-32" style="font-size: 10.722222222222pt;" aria-label="дайджест (22 элемента)" data-wpel-link="internal">дайджест</a>
<a href="https://otus.ru/journal/tag/zapis-vebinara/" class="tag-cloud-link tag-link-226 tag-link-position-33" style="font-size: 14.902777777778pt;" aria-label="запись вебинара (61 элемент)" data-wpel-link="internal">запись вебинара</a>
<a href="https://otus.ru/journal/tag/zapis-uroka/" class="tag-cloud-link tag-link-272 tag-link-position-34" style="font-size: 16.069444444444pt;" aria-label="запись урока (80 элементов)" data-wpel-link="internal">запись урока</a>
<a href="https://otus.ru/journal/tag/informacionnaya-bezopasnost/" class="tag-cloud-link tag-link-232 tag-link-position-35" style="font-size: 10.138888888889pt;" aria-label="информационная безопасность (19 элементов)" data-wpel-link="internal">информационная безопасность</a>
<a href="https://otus.ru/journal/tag/karera-v-it/" class="tag-cloud-link tag-link-292 tag-link-position-36" style="font-size: 9.9444444444444pt;" aria-label="карьера в IT (18 элементов)" data-wpel-link="internal">карьера в IT</a>
<a href="https://otus.ru/journal/tag/podborka/" class="tag-cloud-link tag-link-7 tag-link-position-37" style="font-size: 12.666666666667pt;" aria-label="подборка (35 элементов)" data-wpel-link="internal">подборка</a>
<a href="https://otus.ru/journal/tag/podborka-statej/" class="tag-cloud-link tag-link-219 tag-link-position-38" style="font-size: 15.777777777778pt;" aria-label="подборка статей (75 элементов)" data-wpel-link="internal">подборка статей</a>
<a href="https://otus.ru/journal/tag/programmirovanie/" class="tag-cloud-link tag-link-65 tag-link-position-39" style="font-size: 22pt;" aria-label="программирование (332 элемента)" data-wpel-link="internal">программирование</a>
<a href="https://otus.ru/journal/tag/proekt/" class="tag-cloud-link tag-link-321 tag-link-position-40" style="font-size: 11.888888888889pt;" aria-label="проект (29 элементов)" data-wpel-link="internal">проект</a>
<a href="https://otus.ru/journal/tag/proektnaya-rabota/" class="tag-cloud-link tag-link-310 tag-link-position-41" style="font-size: 11.597222222222pt;" aria-label="проектная работа (27 элементов)" data-wpel-link="internal">проектная работа</a>
<a href="https://otus.ru/journal/tag/seti/" class="tag-cloud-link tag-link-181 tag-link-position-42" style="font-size: 12.958333333333pt;" aria-label="сети (38 элементов)" data-wpel-link="internal">сети</a>
<a href="https://otus.ru/journal/tag/testirovanie/" class="tag-cloud-link tag-link-69 tag-link-position-43" style="font-size: 13.930555555556pt;" aria-label="тестирование (48 элементов)" data-wpel-link="internal">тестирование</a>
<a href="https://otus.ru/journal/tag/upravlenie-komandoj/" class="tag-cloud-link tag-link-63 tag-link-position-44" style="font-size: 11.694444444444pt;" aria-label="управление командой (28 элементов)" data-wpel-link="internal">управление командой</a>
<a href="https://otus.ru/journal/tag/habr-2/" class="tag-cloud-link tag-link-203 tag-link-position-45" style="font-size: 13.930555555556pt;" aria-label="хабр (48 элементов)" data-wpel-link="internal">хабр</a></div>
</li>
</ul>
</div>
</aside>
</div> <!-- .ts-row -->
</div> <!-- .main -->
<footer class="main-footer dark bold">
<section class="lower-footer cf">
<div class="wrap">
<div class="links">
<div class="menu-menju-navykov-container"><ul id="menu-menju-navykov-1" class="menu"><li class="menu-item menu-item-type-custom menu-item-object-custom menu-item-10413"><a href="https://otus.ru/categories/programming/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">Программирование<span class="wpel-icon wpel-image wpel-icon-6"></span></a></li>
<li class="menu-item menu-item-type-custom menu-item-object-custom menu-item-10414"><a href="https://otus.ru/categories/architecture/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">Архитектура<span class="wpel-icon wpel-image wpel-icon-6"></span></a></li>
<li class="menu-item menu-item-type-custom menu-item-object-custom menu-item-10415"><a href="https://otus.ru/categories/operations/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">Инфраструктура<span class="wpel-icon wpel-image wpel-icon-6"></span></a></li>
<li class="menu-item menu-item-type-custom menu-item-object-custom menu-item-10416"><a href="https://otus.ru/categories/information-security-courses/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">Безопасность<span class="wpel-icon wpel-image wpel-icon-6"></span></a></li>
<li class="menu-item menu-item-type-custom menu-item-object-custom menu-item-10417"><a href="https://otus.ru/categories/data-science/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">Data Science<span class="wpel-icon wpel-image wpel-icon-6"></span></a></li>
<li class="menu-item menu-item-type-custom menu-item-object-custom menu-item-10418"><a href="https://otus.ru/categories/gamedev/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">GameDev<span class="wpel-icon wpel-image wpel-icon-6"></span></a></li>
<li class="menu-item menu-item-type-custom menu-item-object-custom menu-item-10419"><a href="https://otus.ru/categories/marketing-business/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">Управление<span class="wpel-icon wpel-image wpel-icon-6"></span></a></li>
<li class="menu-item menu-item-type-custom menu-item-object-custom menu-item-10420"><a href="https://otus.ru/categories/analytics/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">Аналитика и анализ<span class="wpel-icon wpel-image wpel-icon-6"></span></a></li>
<li class="menu-item menu-item-type-custom menu-item-object-custom menu-item-10421"><a href="https://otus.ru/categories/testing/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">Тестирование<span class="wpel-icon wpel-image wpel-icon-6"></span></a></li>
</ul></div> </div>
<p class="copyright"> © 2015-2026 OTUS </p>
<div class="to-top">
<a href="#" class="back-to-top"><i class="fa fa-angle-up"></i> Top</a>
</div>
</div>
</section>
</footer>
</div> <!-- .main-wrap -->
<div class="mobile-menu-container off-canvas" id="mobile-menu">
<a href="#" class="close"><i class="fa fa-times"></i></a>
<div class="logo">
</div>
<ul class="mobile-menu"></ul>
</div>
<div class="search-modal-wrap">
<div class="search-modal-box" role="dialog" aria-modal="true">
<form method="get" class="search-form" action="https://otus.ru/journal/">
<input type="search" class="search-field" name="s" placeholder="Search..." value="" required />
<button type="submit" class="search-submit visuallyhidden">Submit</button>
<p class="message">
Type above and press <em>Enter</em> to search. Press <em>Esc</em> to cancel. </p>
</form>
</div>
</div>
<script type="text/javascript" src="https://otus.ru/journal/wp-content/plugins/clearfy/components/comments-plus/assets/js/url-span.js" id="wbcr-comments-plus-url-span-js"></script>
<script type="text/javascript" id="ez-toc-scroll-scriptjs-js-extra">
/* <![CDATA[ */
var eztoc_smooth_local = {"scroll_offset":"30"};
/* ]]> */
</script>
<script type="text/javascript" src="https://otus.ru/journal/wp-content/plugins/easy-table-of-contents/assets/js/smooth_scroll.min.js" id="ez-toc-scroll-scriptjs-js"></script>
<script type="text/javascript" src="https://otus.ru/journal/wp-content/plugins/easy-table-of-contents/vendor/js-cookie/js.cookie.min.js" id="ez-toc-js-cookie-js"></script>
<script type="text/javascript" src="https://otus.ru/journal/wp-content/plugins/easy-table-of-contents/vendor/sticky-kit/jquery.sticky-kit.min.js" id="ez-toc-jquery-sticky-kit-js"></script>
<script type="text/javascript" id="ez-toc-js-js-extra">
/* <![CDATA[ */
var ezTOC = {"smooth_scroll":"1","visibility_hide_by_default":"","scroll_offset":"30","fallbackIcon":"<span class=\"\"><span class=\"eztoc-hide\" style=\"display:none;\">Toggle<\/span><span class=\"ez-toc-icon-toggle-span\"><svg style=\"fill: #999;color:#999\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" class=\"list-377408\" width=\"20px\" height=\"20px\" viewBox=\"0 0 24 24\" fill=\"none\"><path d=\"M6 6H4v2h2V6zm14 0H8v2h12V6zM4 11h2v2H4v-2zm16 0H8v2h12v-2zM4 16h2v2H4v-2zm16 0H8v2h12v-2z\" fill=\"currentColor\"><\/path><\/svg><svg style=\"fill: #999;color:#999\" class=\"arrow-unsorted-368013\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"10px\" height=\"10px\" viewBox=\"0 0 24 24\" version=\"1.2\" baseProfile=\"tiny\"><path d=\"M18.2 9.3l-6.2-6.3-6.2 6.3c-.2.2-.3.4-.3.7s.1.5.3.7c.2.2.4.3.7.3h11c.3 0 .5-.1.7-.3.2-.2.3-.5.3-.7s-.1-.5-.3-.7zM5.8 14.7l6.2 6.3 6.2-6.3c.2-.2.3-.5.3-.7s-.1-.5-.3-.7c-.2-.2-.4-.3-.7-.3h-11c-.3 0-.5.1-.7.3-.2.2-.3.5-.3.7s.1.5.3.7z\"\/><\/svg><\/span><\/span>"};
/* ]]> */
</script>
<script type="text/javascript" src="https://otus.ru/journal/wp-content/plugins/easy-table-of-contents/assets/js/front.min.js" id="ez-toc-js-js"></script>
<script type="text/javascript" src="https://otus.ru/journal/wp-content/themes/contentberg/js/custom-script.js" id="custom-script-js"></script>
<script type="text/javascript" src="https://otus.ru/journal/wp-content/themes/contentberg/js/magnific-popup.js" id="magnific-popup-js"></script>
<script type="text/javascript" src="https://otus.ru/journal/wp-content/themes/contentberg/js/jquery.fitvids.js" id="jquery-fitvids-js"></script>
<script type="text/javascript" src="https://otus.ru/journal/wp-includes/js/imagesloaded.min.js" id="imagesloaded-js"></script>
<script type="text/javascript" src="https://otus.ru/journal/wp-content/themes/contentberg/js/object-fit-images.js" id="object-fit-images-js"></script>
<script type="text/javascript" id="contentberg-theme-js-extra">
/* <![CDATA[ */
var Bunyad = {"custom_ajax_url":"\/journal\/oop-na-c-pishem-videopleer\/"};
/* ]]> */
</script>
<script type="text/javascript" src="https://otus.ru/journal/wp-content/themes/contentberg/js/theme.js" id="contentberg-theme-js"></script>
<script type="text/javascript" src="https://otus.ru/journal/wp-content/themes/contentberg/js/theia-sticky-sidebar.js" id="theia-sticky-sidebar-js"></script>
<script type="text/javascript" src="https://otus.ru/journal/wp-content/themes/contentberg/js/jquery.slick.js" id="jquery-slick-js"></script>
<script type="text/javascript" src="https://otus.ru/journal/wp-content/themes/contentberg/js/jarallax.js" id="jarallax-js"></script>
<script type="text/javascript" src="https://otus.ru/journal/wp-includes/js/masonry.min.js" id="masonry-js"></script>
<script type="text/javascript" src="https://otus.ru/journal/wp-includes/js/jquery/jquery.masonry.min.js" id="jquery-masonry-js"></script>
</body>
</html>
<!-- Cache served by breeze CACHE - Last modified: Mon, 09 Mar 2026 17:18:10 GMT -->