Writing a VLC plugin for learning English

image



In this article I will talk about how to write a plugin in C for the VLC media player. I wrote my plugin to simplify watching TV shows and films in English. The idea of ​​creating this plugin is described in the sections Idea and Finding a solution . Technical details of the plugin implementation are given in the Hello World plugin and Implementation sections. What happened in the end and how to use it can be found in the last section, Result .



The source code for the project is available on GitHub .



Idea



The idea of ​​learning a foreign language while watching my favorite series is not new, but I personally always had problems with its implementation. It’s very difficult to watch a series or a movie when you don’t understand half of what they say. Of course, you can turn on subtitles, but if an unfamiliar word or expression is encountered in a speech, it won’t become clearer that it will be duplicated by the text. And I did not like watching the series with Russian subtitles at all - the brain switches to its native language and ceases to perceive foreign speech. I read somewhere that first you need to watch the series in Russian, and then in the original. But this approach didn’t suit me either. Firstly, where to take so much time to watch the same thing several times, and secondly, to watch the second time is not so interesting anymore - motivation is lost.



Despite all the difficulties with watching foreign TV shows, I can pretty well read technical documentation, articles and books in English. I like to read books on the Kindle electronic reader, because the dictionary function is cool there - you can find a translation of an unfamiliar word with one touch of the screen. It is convenient to read English-language articles and sites by installing a special extension for translation in the browser — I use the Yandex.Translation extension. This approach allows you to read and understand English texts, without being distracted by the search for unfamiliar words.



I thought, why not use the same approach for watching TV shows - we turn on the series in English as soon as an incomprehensible phrase is found, switch to the Russian audio track and rewind a little back. Next, we continue to watch the series in English.



Search for a solution



In fact, all the functionality I need is already available in many popular media players. The only thing I would like to switch the audio track and rewind the video a few seconds ago with the click of a button. It would also be great if, after translating an incomprehensible fragment, the media player itself switched the audio track back to English. Well, it would be nice to be able to repeat the previously translated fragment with the English track.



That is, I need a media player for which you can write plugins. It is also desirable that it be cross-platform, since I use a PC under Windows and a laptop under Linux. My choice immediately fell on the VLC. On habr I even found an article in which @Idunno tells how to write a VLC extension on LUA. By the way, he also wrote this extension for learning English) Unfortunately, this extension does not work in the latest versions of VLC (older than 2.0.5). Due to the unstable operation of the LUA API, the ability to add callback functions through which it was possible to process keyboard events in the LUA extension was removed. In README , a link to the mailing list of VLC developers discussing this issue leads to its extension on GitHub @Idunno .



Thus, to implement my idea, an extension to LUA will not work, you need to write a plugin in C. And although I wrote something in C the last time about 7 years ago, when I was at university, I decided to try it.



Hello World plugin



It is worth noting that the VLC media player has pretty good documentation . I learned from it that the development of a media player uses a modular approach. VLC consists of several independent modules that implement certain functionality, and the kernel ( libVLCCore ), which manages these modules. There are two types of modules: internal ( in-tree ) and external ( out-of-tree ). The source code of the internal modules is stored in the same repository with the kernel code. External modules are developed and assembled independently of the VLC media player. Actually, the latter are what are called plugins.



The documentation also has an article on how to write your plug-in (module) in C. This article provides the source code for a simple plug-in that, when VLC starts up, displays a welcome message “ Hello, <name> ” to the console (the value <name> is taken from the plugin settings). Running a little ahead, I will say that in the above example, add the following line after set_category(CAT_INTERFACE)



:



 set_subcategory( SUBCAT_INTERFACE_CONTROL )
      
      





Great, it remains only to assemble the plugin and test its work. There is also an instruction for building an external plugin. Here you should pay attention to the Internationalization section, which describes how localization works in VLC. In short, for external plugins you need to define macros N_()



, _()



:



 #define DOMAIN "vlc-myplugin" #define _(str) dgettext(DOMAIN, str) #define N_(str) (str)
      
      





For assembly, it is proposed to use the good old Makefile or Autotools. I decided to go the simple way and chose the Makefile. In the Makefile you need to remember to define the MODULE_STRING



variable - this is the identifier of our plugin. I also tweaked the work with directories a bit - now they are defined through pkg-config . As a result, the following files were obtained:



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
      
      





The easiest way to build a plugin for Linux. To do this, you need to install, in fact, the VLC media player itself, as well as the files and tools necessary for building the plug-in. On Debian / Ubuntu, this can be done with the following command:



 sudo apt-get install vlc libvlc-dev libvlccore-dev gcc make pkg-config
      
      





Actually, everything is ready, we collect and install our plugin using the command:



 sudo make install
      
      





To test the plugin, run VLC also from the console:



 vlc
      
      





Unfortunately, we did not see any “ Hello world ”. The thing is that the plugin must first be enabled. To do this, open the settings ( Tools > Preferences ), switch to the advanced view (select All in the Show settings group) and find in the tree on the left panel Interface > Control interfaces - check the box next to our Hello interface plugin.







We save the settings and restart the VLC.







Build a plugin for Windows



With Windows, things are a little more complicated. To build the plugin, you need to download sdk, which contains libraries, header and configuration files of VLC. Previously, sdk was part of the regular VLC assembly and could be found in the program installation folder. Now it comes as a separate media player assembly. For example, for VLC version 3.0.8, this assembly can be downloaded at ftp://ftp.videolan.org/pub/videolan/vlc/3.0.8/win64/vlc-3.0.8-win64.7z (it is important to download 7z -archive).



Copy the contents of the archive to a folder, for example, to C: \ Projects . In addition to sdk, the archive also contains the media player itself, which can be used to test and debug the plug-in.



So that our Makefile can be used to build and install the plugin, you need to fix the file C: \ Projects \ vlc-3.0.8 \ sdk \ lib \ pkgconfig \ vlc-plugin.pc , indicating the correct path to the sdk folder in the prefix and pluginsdir variables and plugins respectively:



 prefix=/c/Projects/vlc-3.0.8/sdk pluginsdir=/c/Projects/vlc-3.0.8/plugins
      
      





To build under Windows, we also need to install a compiler and other utilities. All the necessary software can be obtained by installing the MSYS2 environment. The project website has detailed installation instructions. In short, immediately after installation you need to open the console ( C: \ msys64 \ msys2.exe ) and update the MSYS2 packages using the command:



 pacman -Syu
      
      





Next, close the MSYS2 terminal window, then open it again and run the command



 pacman -Su
      
      





After updating all packages, you need to install the toolchain:



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





Now that all the necessary packages are installed, you can start building the plugin. I modified the Makefile a bit so that it can build the plugin both under Linux and under Windows. In addition, I had to remove some unsupported MinGW build parameters, as a result the Makefile began to look like this:



Makefile for 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
      
      





Since MSYS2 does not know anything about our sdk for VLC, before building it is necessary to add the path to the pkgconfig folder from this sdk to the PKG_CONFIG_PATH environment variable . Open the MinGW console ( C: \ msys64 \ mingw64.exec ) and execute the commands:



 export PKG_CONFIG_PATH=/c/projects/vlc-3.0.8/sdk/lib/pkgconfig:$PKG_CONFIG_PATH make install
      
      





To test the plugin, run VLC also from the console:



 /c/projects/vlc-3.0.8/vlc
      
      





As in the case of Linux, go to the settings and turn on our plugin. We save the settings and restart the VLC.



Plugin implementation



To implement my plugin, I needed to understand how to control the media player (change the audio track, rewind) and how to handle keyboard keystroke events. To understand all this, I turned to the documentation . Also on the Internet, I found a couple of interesting articles that shed light on the architecture of the media player: The architecture of VLC media framework and VLC media player API Documentation .



VLC consists of a large number of independent modules (400+). Each module must provide information about the type of functionality it implements, as well as the initialization / finalization functions. This information is described in the vlc_module_begin () - vlc_module_end () block using the set_capability () and set_callbacks () macros. The module initialization / finalization functions (usually called Open and Close ) have the following signature:



 static int Open(vlc_object_t *) static void Close(vlc_object_t *)
      
      





vlc_object_t is the base type for representing data in VLC, from which all others are inherited (see the article Object_Management ). The pointer to vlc_object_t needs to be cast to a specific data type in accordance with the functionality that the module implements. To control the media player, I set the interface value in the set_capability () macro. Accordingly, in the Open and Close functions, I need to cast vlc_object_t to intf_thread_t .



The interaction between the modules is based on the observer design pattern. VLC provides an “object variables” mechanism (see Variables ), with which you can add variables to instances of type vlc_object_t (and its derivatives). Modules can exchange data through these variables. You can also attach a callback function to the variable, which will be called when the value of this variable changes.



As an example, consider the Hotkeys module ( modules / control / hotkeys.c ), which is responsible for handling hotkey events. In the Open function, an ActionEvent callback function is hung on the key-action variable :



 var_AddCallback( p_intf->obj.libvlc, "key-action", ActionEvent, p_intf );
      
      





A pointer to vlc_object_t , a variable name, a callback function and a pointer to void are passed to the var_AddCallback function to transfer arbitrary data, which is then forwarded to the specified callback function. The signature of the callback function is shown below.



 static int ActionEvent(vlc_object_t *, char const *, vlc_value_t, vlc_value_t, void *)
      
      





A pointer to vlc_object_t , the name of the variable, the old and new values ​​of this variable (in this case, the identifier of the corresponding pressed hotkey combination of the action), as well as the pointer to any additional data specified when adding the callback function, are passed to the callback function .



Direct processing of hotkey events is performed in the PutAction function, which is called inside the ActionEvent callback function. The PutAction function accepts the identifier of the event of pressing the hotkey combination ( i_action ) as an input and, using the switch statement, performs the corresponding actions.



For example, a rewind event corresponds to ACTIONID_JUMP_BACKWARD_SHORT



. To perform the corresponding action, the rewind interval is taken from the VLC settings (from the variable short-jump-size ):



 mtime_t it = var_InheritInteger( p_input, varname );
      
      





To rewind the file being played, just set the time-offset variable to the value corresponding to the time (in microseconds) by which you want to shift the playback:



 var_SetInteger( p_input, "time-offset", it * sign * CLOCK_FREQ );
      
      





For fast-forward you need to specify a positive value, for fast-reverse - negative. The CLOCK_FREQ constant is used to convert seconds to microseconds.



Similarly, the audio track changes ( ACTIONID_AUDIO_TRACK



event). Only the audio-es variable responsible for the audio track can accept a limited set of values ​​(in accordance with the audio tracks available in the file being played). You can get a list of possible values ​​of a variable using the var_Change () function:



 vlc_value_t list, list2; var_Change( p_input, "audio-es", VLC_VAR_GETCHOICES, &list, &list2 );
      
      





In addition to the list of values, this function also allows you to get a list of descriptions of these values ​​(in this case, the name of the audio tracks). Now we can change the audio track using the var_Set () function:



 var_Set( p_input, "audio-es", list.p_list->p_values[i] );
      
      





How to manage the media player figured out, it remains to learn how to handle keyboard events. Unfortunately, I couldn’t add a new hotkey. All hotkeys are hardcoded in the VLC kernel code ( src / misc / actions.c ). Therefore, I added a handler for lower-level keyboard keystroke events, hanging my callback function to change the key-pressed variable:



 var_AddCallback( p_intf->obj.libvlc, "key-pressed", KeyboardEvent, p_intf );
      
      





The key-pressed variable stores the character code (in Unicode) corresponding to the last key pressed. For example, when you press a key with the number "1" , the key-pressed variable will be assigned the value 49 (0x00000031 in the 16th number system). You can view other character codes at unicode-table.com . In addition, the value of the key-pressed variable takes into account the pressing of modifier keys, the fourth significant byte is allocated for them. So, for example, when pressing the “ Ctrl + 1key combination, the key-pressed variable will be assigned the value 0x 04 000031 (00000 1 00 00000000 00000000 00110001 2 ). For clarity, the table below shows the values ​​of various key combinations:

Keyboard shortcut Value
Ctrl + 1 00000 1 00 00000000 00000000 00110001 2
Alt + 1 0000000 1 00000000 00000000 00110001 2
Ctrl + Alt + 1 00000 1 0 1 00000000 00000000 00110001 2
Shift + 1 000000 1 0 00000000 00000000 00100001 2
Pay attention to the value when pressing the “ Shift + 1 ” combination. Since in this case the “ ! ", Then the value of the first byte will correspond to the code of this character in Unicode (0x00000021).



Result



I named my TIP plugin an acronym for the phrase “translate it, please”, and tip can also be translated as “hint”. The source code of the plugin is published on GitHub , where you can also download ready-made plug-in assemblies for Windows and Linux.



To install the plugin, you need to copy the file libtip_plugin.dll (libtip_plugin.so for Linux) to the <path-to-vlc> / plugins folder. On Windows, VLC is usually installed in the C: \ Program Files \ VideoLAN \ VLC folder. On Linux, you can find the installation folder using the command:



 whereis vlc
      
      





In Ubuntu, for example, VLC is installed in / usr / lib / x86_64-linux-gnu / vlc .



Next, you will need to restart VLC, then in the main menu open Tools > Preferences , switch to the advanced view (select All in the Show settings group), go to the Interface > Control section on the left panel and check the box next to TIP (translate it, please) . Then again you will need to restart the VLC.







In the plugin settings, you can specify the numbers of the main and auxiliary (for translation) audio tracks, as well as the time (in seconds) by which the plugin will rewind to repeat with the auxiliary audio track.







To control the plugin, I added the following keyboard shortcuts:





During execution of the translation and retry commands, the plugin displays the messages “TIP: translate” and “TIP: repeat” in the upper left corner , respectively.







From my experience using the plugin, I can say that, in general, I am pleased with the result. The main thing is to choose the right content, if at least half of the foreign speech used there is understood, the plugin will help translate the rest. Otherwise, the plugin will probably be useless.



All Articles