В данной статье я расскажу о том, как написать плагин на языке C для медиаплеера VLC. Я написал свой плагин для упрощения просмотра сериалов и фильмов на английском языке. Идея создания этого плагина описывается в разделах Идея и Поиск решения. Технические детали реализации плагина приведены в разделах Hello World плагин и Реализация. О том, что получилось в итоге и как этим пользоваться можно прочитать в последнем разделе, Результат.
Исходный код проекта доступен на GitHub.
Идея
Идея изучать иностранный язык за просмотром любимого сериала не нова, но вот с ее воплощением в жизнь лично у меня всегда возникали проблемы. Очень сложно смотреть сериал или фильм, когда не понимаешь половину того, что там говорят. Конечно, можно включить субтитры, но если в речи встретится незнакомое слово или выражение, то от того, что оно будет продублировано текстом, яснее не станет. А смотреть сериал с русскими субтитрами мне совсем не понравилось —
Несмотря на все сложности с просмотром иностранных сериалов, я довольно неплохо могу читать техническую документацию, статьи и книги на английском. Книги мне нравится читать на электронной читалке Kindle, так как там классно реализована функция работы со словарями — можно одним касанием экрана найти перевод незнакомого слова. Англоязычные статьи и сайты удобно читать, установив в браузере специальное расширение для перевода, — я пользуюсь расширением Яндекс.Перевод. Такой подход позволяет читать и понимать английские тексты, не сильно отвлекаясь на поиск незнакомых слов.
Я подумал, почему бы не применить тот же подход для просмотра сериалов — включаем серию на английском, как только встречается непонятная фраза, переключаемся на русскую аудиодорожку и отматываем немного назад. Далее продолжаем смотреть серию на английском.
Поиск решения
На самом деле, вся необходимая мне функциональность уже доступна во многих популярных медиаплеерах. Единственное, мне бы хотелось переключать аудиодорожку и отматывать видео на несколько секунд назад нажатием одной кнопки. Также было бы здорово, если бы после перевода непонятного фрагмента медиаплеер сам переключал аудиодорожку обратно на английскую. Ну и еще неплохо было бы иметь возможность повторять переведенный ранее фрагмент с английской дорожкой.
То есть, мне нужен медиаплеер, под который можно писать плагины. Желательно еще, чтобы он был кроссплатформенным, так как я пользуюсь ПК под Windows и ноутбуком под Linux. Мой выбор сразу же пал на VLC. На хабре я даже нашел статью, в которой @Idunno рассказывает, как написать расширение VLC на LUA. Кстати, это расширение он тоже написал для изучения английского) К сожалению, данное расширение не работает в последних версиях VLC (старше 2.0.5). Из-за нестабильной работы из LUA API убрали возможность добавления callback-функций, через которые в LUA-расширении можно было обрабатывать события клавиатуры. В README к своему расширению на GitHub @Idunno приводит ссылку на mailing list разработчиков VLC с обсуждением данной проблемы.
Таким образом, для реализации моей идеи расширение на LUA не подойдет, нужно писать плагин на C. И хоть я и писал что-либо на C последний раз лет 7 назад, еще в университете, решил попробовать.
Hello World плагин
Стоит отметить, что у медиаплеера VLC достаточно хорошая документация. Из нее я узнал, что при разработке медиаплеера используется модульный подход. VLC состоит из нескольких независимых модулей, реализующих определенную функциональность, и ядра (libVLCCore), которое управляет этими модулями. При этом модули бывают двух типов: внутренние (in-tree) и внешние (out-of-tree). Исходный код внутренних модулей хранится в одном репозитории с кодом ядра. Внешние модули разрабатываются и собираются независимо от медиаплеера VLC. Собственно, последние и есть то, что называют плагинами.
В документации также есть статья о том, как написать свой плагин (модуль) на языке C. В этой статье приводится исходный код простого плагина, который при запуске VLC выводит на консоль приветственное сообщение «Hello, <name>» (значение <name> берется из настроек плагина). Забегая немного вперед, скажу, что в приведенном примере нужно добавить следующую строчку после set_category(CAT_INTERFACE)
:
set_subcategory( SUBCAT_INTERFACE_CONTROL )
Отлично, осталось только собрать плагин и протестировать его работу. По сборке внешнего плагина тоже есть инструкция. Тут стоит обратить внимание на раздел Internationalization, в котором описывается, как работает локализация в VLC. Если коротко, то для внешних плагинов требуется определять макросы N_()
, _()
:
#define DOMAIN "vlc-myplugin"
#define _(str) dgettext(DOMAIN, str)
#define N_(str) (str)
Для сборки предлагается использовать старый добрый Makefile либо Autotools. Я решил пойти простым путем и выбрал Makefile. В Makefile нужно не забыть определить переменную MODULE_STRING
— это идентификатор нашего плагина. Также я немного подправил работу с директориями — теперь они определяются через pkg-config. В итоге получились следующие файлы:
/**
* @file hello.c
* @brief Hello world interface VLC module example
*/
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif
#define DOMAIN "vlc-myplugin"
#define _(str) dgettext(DOMAIN, str)
#define N_(str) (str)
#include <stdlib.h>
/* VLC core API headers */
#include <vlc_common.h>
#include <vlc_plugin.h>
#include <vlc_interface.h>
/* Forward declarations */
static int Open(vlc_object_t *);
static void Close(vlc_object_t *);
/* Module descriptor */
vlc_module_begin()
set_shortname(N_("Hello"))
set_description(N_("Hello interface"))
set_capability("interface", 0)
set_callbacks(Open, Close)
set_category(CAT_INTERFACE)
set_subcategory( SUBCAT_INTERFACE_CONTROL )
add_string("hello-who", "world", "Target", "Whom to say hello to.", false)
vlc_module_end ()
/* Internal state for an instance of the module */
struct intf_sys_t
{
char *who;
};
/**
* Starts our example interface.
*/
static int Open(vlc_object_t *obj)
{
intf_thread_t *intf = (intf_thread_t *)obj;
/* Allocate internal state */
intf_sys_t *sys = malloc(sizeof (*sys));
if (unlikely(sys == NULL))
return VLC_ENOMEM;
intf->p_sys = sys;
/* Read settings */
char *who = var_InheritString(intf, "hello-who");
if (who == NULL)
{
msg_Err(intf, "Nobody to say hello to!");
goto error;
}
sys->who = who;
msg_Info(intf, "Hello %s!", who);
return VLC_SUCCESS;
error:
free(sys);
return VLC_EGENERIC;
}
/**
* Stops the interface.
*/
static void Close(vlc_object_t *obj)
{
intf_thread_t *intf = (intf_thread_t *)obj;
intf_sys_t *sys = intf->p_sys;
msg_Info(intf, "Good bye %s!", sys->who);
/* Free internal state */
free(sys->who);
free(sys);
}
LD = ld
CC = cc
PKG_CONFIG = pkg-config
INSTALL = install
CFLAGS = -g -O2 -Wall -Wextra
LDFLAGS =
LIBS =
VLC_PLUGIN_CFLAGS := $(shell $(PKG_CONFIG) --cflags vlc-plugin)
VLC_PLUGIN_LIBS := $(shell $(PKG_CONFIG) --libs vlc-plugin)
VLC_PLUGIN_DIR := $(shell $(PKG_CONFIG) --variable=pluginsdir vlc-plugin)
plugindir = $(VLC_PLUGIN_DIR)/misc
override CC += -std=gnu99
override CPPFLAGS += -DPIC -I. -Isrc
override CFLAGS += -fPIC
override LDFLAGS += -Wl,-no-undefined,-z,defs
override CPPFLAGS += -DMODULE_STRING="hello"
override CFLAGS += $(VLC_PLUGIN_CFLAGS)
override LIBS += $(VLC_PLUGIN_LIBS)
all: libhello_plugin.so
install: all
mkdir -p -- $(DESTDIR)$(plugindir)
$(INSTALL) --mode 0755 libhello_plugin.so $(DESTDIR)$(plugindir)
install-strip:
$(MAKE) install INSTALL="$(INSTALL) -s"
uninstall:
rm -f $(plugindir)/libhello_plugin.so
clean:
rm -f -- libhello_plugin.so src/*.o
mostlyclean: clean
SOURCES = hello.c
$(SOURCES:%.c=src/%.o): $(SOURCES:%.c=src/%.c)
libhello_plugin.so: $(SOURCES:%.c=src/%.o)
$(CC) $(LDFLAGS) -shared -o $@ $^ $(LIBS)
.PHONY: all install install-strip uninstall clean mostlyclean
Проще всего собрать плагин под Linux. Для этого потребуется установить, собственно, сам медиаплеер VLC, а также необходимые для сборки плагина файлы и инструменты. В Debian/Ubuntu это можно сделать с помощью следующей команды:
sudo apt-get install vlc libvlc-dev libvlccore-dev gcc make pkg-config
Собственно, все готово, собираем и устанавливаем наш плагин с помощью команды:
sudo make install
Для проверки работы плагина запускаем VLC также из консоли:
vlc
К сожалению, никакого «Hello world» мы не увидели. Все дело в том, что плагин нужно сначала включить. Для этого открываем настройки (Tools > Preferences), переключаемся на расширенный вид (выбираем All в группе Show settings) и находим в дереве на панели слева Interface > Control interfaces — ставим галочку напротив нашего плагина Hello interface.
Сохраняем настройки и перезапускаем VLC.
Сборка плагина под Windows
С Windows все немного сложнее. Для сборки плагина потребуется скачать sdk, который содержит библиотеки, заголовочные и конфигурационные файлы VLC. Раньше sdk входил в обычную сборку VLC и найти его можно было в папке установки программы. Теперь же он поставляется в виде отдельной сборки медиаплеера. Например, для VLC версии 3.0.8 эту сборку можно скачать по ссылке ftp://ftp.videolan.org/pub/videolan/vlc/3.0.8/win64/vlc-3.0.8-win64.7z (важно скачивать именно 7z-архив).
Копируем содержимое архива в какую-нибудь папку, например, в С:Projects. Кроме sdk архив содержит и сам медиаплеер, который можно использовать для тестирования и отладки плагина.
Чтобы наш Makefile можно было использовать для сборки и установки плагина, нужно поправить файл C:Projectsvlc-3.0.8sdklibpkgconfigvlc-plugin.pc, указав в переменных prefix и pluginsdir корректный путь до папки с sdk и plugins соответственно:
prefix=/c/Projects/vlc-3.0.8/sdk
pluginsdir=/c/Projects/vlc-3.0.8/plugins
Для сборки под Windows нам также потребуется установить компилятор и другие утилиты. Все необходимое ПО можно получить, установив среду MSYS2. На сайте проекта есть подробная инструкция по установке. Если коротко, то сразу после установки нужно открыть консоль (C:msys64msys2.exe) и обновить пакеты MSYS2 с помощью команды:
pacman -Syu
Далее нужно закрыть окно терминала MSYS2, затем открыть его еще раз и выполнить команду
pacman -Su
После обновления всех пакетов нужно установить тулчейн:
pacman -S base-devel mingw-w64-x86_64-toolchain
Теперь, когда все необходимые пакеты установлены, можно приступать к сборке плагина. Я немного доработал Makefile, чтобы он мог собирать плагин как под Linux так и под Windows. Кроме того, пришлось убрать некоторые неподдерживаемые MinGW параметры сборки, в итоге Makefile стал выглядеть так:
LD = ld
CC = cc
PKG_CONFIG = pkg-config
INSTALL = install
CFLAGS = -g -O2 -Wall -Wextra
LDFLAGS =
LIBS =
VLC_PLUGIN_CFLAGS := $(shell $(PKG_CONFIG) --cflags vlc-plugin)
VLC_PLUGIN_LIBS := $(shell $(PKG_CONFIG) --libs vlc-plugin)
VLC_PLUGIN_DIR := $(shell $(PKG_CONFIG) --variable=pluginsdir vlc-plugin)
plugindir = $(VLC_PLUGIN_DIR)/misc
override CC += -std=gnu99
override CPPFLAGS += -DPIC -I. -Isrc
override CFLAGS += -fPIC
override LDFLAGS += -Wl,-no-undefined
override CPPFLAGS += -DMODULE_STRING="hello"
override CFLAGS += $(VLC_PLUGIN_CFLAGS)
override LIBS += $(VLC_PLUGIN_LIBS)
SUFFIX := so
ifeq ($(OS),Windows_NT)
SUFFIX := dll
endif
all: libhello_plugin.$(SUFFIX)
install: all
mkdir -p -- $(DESTDIR)$(plugindir)
$(INSTALL) --mode 0755 libhello_plugin.$(SUFFIX) $(DESTDIR)$(plugindir)
install-strip:
$(MAKE) install INSTALL="$(INSTALL) -s"
uninstall:
rm -f $(plugindir)/libhello_plugin.$(SUFFIX)
clean:
rm -f -- libhello_plugin.$(SUFFIX) src/*.o
mostlyclean: clean
SOURCES = hello.c
$(SOURCES:%.c=src/%.o): $(SOURCES:%.c=src/%.c)
libhello_plugin.$(SUFFIX): $(SOURCES:%.c=src/%.o)
$(CC) $(LDFLAGS) -shared -o $@ $^ $(LIBS)
.PHONY: all install install-strip uninstall clean mostlyclean
Так как MSYS2 ничего не знает о нашем sdk для VLC, то перед сборкой нужно добавить в переменную окружения PKG_CONFIG_PATH путь до папки pkgconfig из этого sdk. Открываем консоль MinGW (C:msys64mingw64.exec) и выполняем команды:
export PKG_CONFIG_PATH=/c/projects/vlc-3.0.8/sdk/lib/pkgconfig:$PKG_CONFIG_PATH
make install
Для проверки работы плагина запускаем VLC также из консоли:
/c/projects/vlc-3.0.8/vlc
Как и в случае с Linux, идем в настройки и включаем наш плагин. Сохраняем настройки и перезапускаем VLC.
Реализация плагина
Для реализации моего плагина мне необходимо было понять, как управлять медиаплеером (менять аудиодорожку, перематывать назад) и как обрабатывать события нажатия клавиш клавиатуры. Чтобы разобраться во всем этом, я обратился к документации. Также в Интернете я нашел пару интересных статей, проливающих свет на архитектуру медиаплеера: The architecture of VLC media framework и VLC media player API Documentation.
VLC состоит из большого количества независимых модулей (400+). Каждый модуль должен предоставлять информацию о типе реализуемой им функциональности, а также функции инициализации/финализации. Данная информация описывается в блоке vlc_module_begin() — vlc_module_end() с помощью макросов set_capability() и set_callbacks(). Функции инициализации/финализации модуля (обычно они называются Open и Close) имеют следующую сигнатуру:
static int Open(vlc_object_t *)
static void Close(vlc_object_t *)
vlc_object_t — это базовый тип для представления данных в VLC, от которого наследуются все остальные (см. статью Object_Management). Указатель на vlc_object_t нужно приводить к конкретному типу данных в соответствии с той функциональностью, которую реализует модуль. Для управления медиаплеером я указал в макросе set_capability() значение interface. Соответственно, в функциях Open и Close мне нужно привести vlc_object_t к intf_thread_t.
Взаимодействие между модулями построено на основе шаблона проектирования observer. VLC предоставляет механизм «object variables» (см. статью Variables), с помощью которого в экземпляры типа vlc_object_t (и его производные) можно добавлять переменные. Через эти переменные модули могут обмениваться данными. Также на переменную можно навесить callback-функцию, которая будет вызываться при изменении значения этой переменной.
В качестве примера можно рассмотреть модуль Hotkeys (modules/control/hotkeys.c), который отвечает за обработку событий нажатия горячих клавиш. В функции Open на переменную key-action навешивается callback-функция ActionEvent:
var_AddCallback( p_intf->obj.libvlc, "key-action", ActionEvent, p_intf );
В функцию var_AddCallback передается указатель на vlc_object_t, имя переменной, callback-функция и указатель на void для передачи произвольных данных, которые потом пробрасываются в указанную callback-функцию. Сигнатура callback-функции приведена ниже.
static int ActionEvent(vlc_object_t *, char const *, vlc_value_t, vlc_value_t, void *)
В качестве параметров в callback-функцию передается указатель на vlc_object_t, имя переменной, старое и новое значение этой переменной (в данном случае, идентификатор соответствующего нажатой комбинации горячих клавиш действия), а также заданный при добавлении callback-функции указатель на какие-либо дополнительные данные.
Непосредственно обработка событий горячих клавиш выполняется в функции PutAction, которая вызывается внутри callback-функции ActionEvent. Функция PutAction принимает на вход идентификатор события нажатия комбинации горячих клавиш (i_action) и с помощью оператора switch выполняет соответствующие действия.
Например, событию перемотки назад соответствует значение ACTIONID_JUMP_BACKWARD_SHORT
. Для выполнения соответствующего действия из настроек VLC берется интервал перемотки (из переменной short-jump-size):
mtime_t it = var_InheritInteger( p_input, varname );
Чтобы перемотать проигрываемый файл, достаточно присвоить переменной time-offset значение, соответствующее времени (в микросекундах), на которое нужно сместить воспроизведение:
var_SetInteger( p_input, "time-offset", it * sign * CLOCK_FREQ );
Для перемотки вперед нужно указать положительное значение, для перемотки назад — отрицательное. Константа CLOCK_FREQ используется для конвертации секунд в микросекунды.
Аналогичным образом происходит смена аудиодорожки (событие ACTIONID_AUDIO_TRACK
). Только отвечающая за аудиодорожку переменная audio-es может принимать ограниченный набор значений (в соответствии с имеющимися в проигрываемом файле аудиодорожками). Получить список возможных значений переменной можно с помощью функции var_Change():
vlc_value_t list, list2;
var_Change( p_input, "audio-es", VLC_VAR_GETCHOICES, &list, &list2 );
Помимо списка значений эта функция также позволяет получить список описаний этих значений (в данном случае название аудиодорожек). Теперь мы можем поменять аудиодорожку с помощью функции var_Set():
var_Set( p_input, "audio-es", list.p_list->p_values[i] );
Как управлять медиаплеером разобрались, осталось научиться обрабатывать события клавиатуры. К сожалению, добавить новую горячую клавишу у меня не получилось. Все горячие клавиши жестко зашиты в коде ядра VLC (src/misc/actions.c). Поэтому я добавил обработчик более низкоуровневых событий нажатия клавиш клавиатуры, повесив свою callback-функцию на изменение переменной key-pressed:
var_AddCallback( p_intf->obj.libvlc, "key-pressed", KeyboardEvent, p_intf );
В переменной key-pressed хранится код символа (в Unicode), соответствующего последней нажатой клавише. Например, при нажатии клавиши с цифрой «1» переменной key-pressed будет присвоено значение 49 (0x00000031 в 16ой системе счисления). Посмотреть коды других символов можно на сайте unicode-table.com. Кроме того, в значении переменной key-pressed учитывается нажатие клавиш-модификаторов, для них отведен четвертый значащий байт. Так, например, при нажатии комбинации клавиш «Ctrl + 1» переменной key-pressed будет присвоено значение 0x04000031 (00000100 00000000 00000000 001100012). Для наглядности в таблице ниже приведены значения различных комбинаций клавиш:
Комбинация клавиш | Значение |
---|---|
Ctrl + 1 | 00000100 00000000 00000000 001100012 |
Alt + 1 | 00000001 00000000 00000000 001100012 |
Ctrl + Alt + 1 | 00000101 00000000 00000000 001100012 |
Shift + 1 | 00000010 00000000 00000000 001000012 |
Обратите внимание на значение при нажатии комбинации «Shift + 1». Так как в этом случае будет выведен символ «!», то и значение первого байта будет соответствовать коду этого символа в Unicode (0x00000021).
Результат
Я назвал свой плагин TIP — акроним от фразы «translate it, please», также tip можно перевести как «подсказка». Исходный код плагина опубликован на GitHub, там же можно скачать готовые сборки плагина под Windows и Linux.
Для установки плагина нужно скопировать файл libtip_plugin.dll (libtip_plugin.so для Linux) в папку <path-to-vlc>/plugins. В Windows VLC обычно устанавливается в папку C:Program FilesVideoLANVLC. В Linux найти папку установки можно с помощью команды:
whereis vlc
В Ubuntu, например, VLC ставится в /usr/lib/x86_64-linux-gnu/vlc.
Далее потребуется перезапустить VLC, затем в главном меню открыть Tools > Preferences, переключиться на расширенный вид (выбрать All в группе Show settings), на панели слева перейти в раздел Interface > Control и поставить галочку напротив пункта TIP (translate it, please). После чего снова потребуется перезапуск VLC.
В настройках плагина можно указать номера основной и вспомогательной (для перевода) аудиодорожек, а также время (в секундах), на которое плагин будет отматывать назад для повтора со вспомогательной аудиодорожкой.
Для управления плагином я добавил следующие горячие клавиши:
- «/» для перевода
- «Shift + /» для повтора переведенного ранее фрагмента видео с основной аудиодорожкой
Во время выполнения команд перевода и повтора плагин отображает в верхнем левом углу сообщения «TIP: translate» и «TIP: repeat» соответственно.
По своему опыту использования плагина могу сказать, что, в целом, я доволен результатом. Главное, правильно подобрать контент, если будет понятна хотя бы половина используемой там иностранной речи, плагин поможет перевести остальное. В противном случае плагин, наверное, будет бесполезен.
Автор: aklexel