Pull to refresh

Пишем VLC плагин для изучения английского

Reading time13 min
Views19K
image

В данной статье я расскажу о том, как написать плагин на языке 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. В итоге получились следующие файлы:

hello.c
/**
 * @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);
}

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,-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:\Projects\vlc-3.0.8\sdk\lib\pkgconfig\vlc-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:\msys64\msys2.exe) и обновить пакеты MSYS2 с помощью команды:

pacman -Syu

Далее нужно закрыть окно терминала MSYS2, затем открыть его еще раз и выполнить команду

pacman -Su

После обновления всех пакетов нужно установить тулчейн:

pacman -S base-devel mingw-w64-x86_64-toolchain

Теперь, когда все необходимые пакеты установлены, можно приступать к сборке плагина. Я немного доработал Makefile, чтобы он мог собирать плагин как под Linux так и под Windows. Кроме того, пришлось убрать некоторые неподдерживаемые MinGW параметры сборки, в итоге Makefile стал выглядеть так:

Makefile для Windows
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:\msys64\mingw64.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 Files\VideoLAN\VLC. В 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» соответственно.



По своему опыту использования плагина могу сказать, что, в целом, я доволен результатом. Главное, правильно подобрать контент, если будет понятна хотя бы половина используемой там иностранной речи, плагин поможет перевести остальное. В противном случае плагин, наверное, будет бесполезен.

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

Настройки выбора аудиодорожки и субтитров автоматически сбрасываются на те, что были до нажатия клавиш перевода/повтора.

Также добавил сборку плагина под macOS.
Tags:
Hubs:
Total votes 52: ↑52 and ↓0+52
Comments28

Articles