Skip to content

Create a plugin module from scratch

Under review: this document corresponds to version 1.0 of paella-core and is under revision.

In this tutorial you will learn how to create from scratch a plugin module for paella-core, and publish it via npm. Before continuing, remember to consult the documentation on paella-core plugins. This tutorial is purely practical, and will not address any theoretical concepts about the paella-core plugin APIs, if you have any questions, you have all the information about plugins in the document above, and in the other documents about the types of plugins in paella-core.

This tutorial code is available at https://github.com/polimediaupv/plugin-module-tutorial.git.

A plugin module for Paella Player is a piece of software that depends on paella-core, and therefore cannot work on its own. Therefore, in order to create a plugin module and test it conveniently, the first thing we need to do is to create a player based on paella-core.

You can use any building system, or even none at all, but in this tutorial we are going to use vite. The first thing we will do is create a Vite project. But let’s organize things a bit. Instead of working directly with the Vite application directory, let’s keep the test application code and the plugin module code separate.

Terminal window
$ mkdir my-plugin-module-project
$ cd my-plugin-module-project
$ npm create vite@latest
Need to install the following packages:
create-vite@6.5.0
Ok to proceed? (y)
> npx
> create-vite
Project name:
example-app
Select a framework:
Vanilla
Select a variant:
JavaScript
Scaffolding project in /Users/fernando/upv/plugin-module-tutorial/example-app...
Done. Now run:
cd example-app
npm install
npm run dev

To learn how to create a player from scratch, you can follow the tutorial series on the subject:

But that is not the purpose of this tutorial, so we will start from the following code for the example application:

example-app/package.json

{
"name": "example-app",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"vite": "6.3.5"
},
"dependencies": {
"@asicupv/paella-core": "^2.1.9"
}
}

example-app/main.js

import { Paella } from '@asicupv/paella-core';
import "@asicupv/paella-core/paella-core.css";
const player = new Paella('playerContainer');
await player.loadManifest();

example-app/index.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Paella Player Tutorial</title>
<style>
body {
margin: 0px;
font-family: sans-serif;
}
#playerContainer {
width: 100vw;
height: 100vh;
}
</style>
</head>
<body>
<div id="playerContainer"></div>
<script type="module" src="/main.js"></script>
</body>
</html>

example-app/public/config/config.json

{
"defaultLayout": "presenter-presentation",
"repositoryUrl": "repo",
"manifestFileName": "data.json",
"plugins": {
"es.upv.paella.dualVideoDynamic": {
"enabled": "true",
"validContent": [
{ "id": "presenter-presentation", "content": ["presenter","presentation"], "icon": "", "title": "Presenter and presentation" }
],
"pipContentIds": [
"presenter-presentation-pip"
]
},
"es.upv.paella.singleVideoDynamic": {
"enabled": "true",
"validContent": [
{ "id": "presenter", "content": ["presenter"], "icon": "", "title": "Presenter" },
{ "id": "presentation", "content": ["presentation"], "icon": "", "title": "Presentation" }
],
"dualVideoContentIds": [
"presenter-presentation"
]
},
"es.upv.paella.dualVideoPiP": {
"enabled": true,
"validContent": [
{ "id": "presenter-presentation-pip", "content": ["presenter","presentation"], "icon": "present-mode-3.svg", "title": "Presenter and presentation" }
],
"dualVideoContentIds": [
"presenter-presentation"
]
},
"es.upv.paella.htmlVideoFormat": {
"enabled": true
},
"es.upv.paella.videoCanvas": {
"enabled": true
},
"es.upv.paella.playPauseButton": {
"enabled": true
},
"es.upv.paella.currentTimeLabel": {
"enabled": true,
"side": "left",
"showTotalTime": true
}
}
}

example-app/public/repo/dual-stream/data.json

{
"metadata": {
"preview": "https://repository.paellaplayer.upv.es/belmar-multiresolution/preview/belmar-preview.jpg"
},
"streams": [
{
"sources": {
"html": [
{
"src": "https://repository.paellaplayer.upv.es/belmar-multiresolution/media/720-presenter.mp4",
"mimetype": "video/mp4"
}
]
},
"content": "presenter",
"role": "mainAudio"
},
{
"sources": {
"html": [
{
"src": "https://repository.paellaplayer.upv.es/belmar-multiresolution/media/720-presentation.mp4",
"mimetype": "video/mp4"
}
]
},
"content": "presentation"
}
]
}

Test the application by running the following command:

Terminal window
$ cd example-app
$ npm install
$ npm run dev

Now you can open your browser and go to http://localhost:5173?id=dual-stream.

Worspaces are a way for npm to manage several packages within the local file system, from a single root package. In our case we are going to use it to join the project of the example application and the plugin project.

We have already created the sample application project, now we are going to create the plugin module project. To begin with, we are going to create a new npm package with Vite and to add a very simple example function. Execute the following command in the root directory of the project (the resulting directory will be placed in the same directory as the example application):

Terminal window
$ npm create vite@latest my-plugin-module
> npx
> create-vite my-plugin-module
Select a framework:
Vanilla
Select a variant:
JavaScript
Scaffolding project in /Users/fernando/upv/plugin-module-tutorial/my-plugin-module...
Done. Now run:
cd my-plugin-module
npm install
npm run dev

Leave only the following files in the my-plugin-module folder:

  • src/main.js
  • index.html
  • package.json

Let’s start by adding two functions to src/main.js, which we will call from the sample application:

src/main.js

export function sayHello() {
console.log("Hello from my plugin!");
}
export function sayGoodbye() {
console.log("Goodbye from my plugin!");
}

To create a library with Vite, the first thing we have to do is to indicate what the name of the library file is going to be. By default, Vite generates the output files in the dist folder with names based on a hash. This is useful in a web application, since it will automatically load the correct version of the library in the browser: since the file name changes every time it is compiled, the browser cannot use the cached version. However, in our case, we want the file name to be always the same, because what we are doing is a biblitoeca, and it doesn’t make sense for the file name to change every time.

To do this, we have to modify the vite.config.js file. It is possible that the file does not exist: depending on the Vite template we use, sometimes it is not created. In that case we have to create it ourselves.

Taking advantage of the fact that we are modifying the Vite configuration file, we are also going to avoid the need to use the index.html file. Both modifications can be done through the build property of the configuration:

import { defineConfig } from "vite";
export default defineConfig({
build: {
outDir: 'dist',
lib: {
entry: 'src/main.js',
name: 'my-plugin-module'
}
}
});

With these changes, we can delete the index.html file.

Compile the library by running the following command in the my-plugin-module folder to generate the my-plugin-module/dist/my-plugin-module.js file:

Terminal window
$ npm run build

To finish with the package definition, modify the my-plugin-module/package.json file to indicate which is the input file of the library:

{
"name": "my-plugin-module",
"private": false,
"version": "0.0.1",
"type": "module",
"exports": {
".": "./dist/my-plugin-module.js"
},
"module": "./dist/my-plugin-module.js",
"main": "./dist/my-plugin-module.js",
"files": [
"dist/my-plugin-module.js"
],
...
}

So far we have a directory containing two packages: the example player and the plugin module. Now let’s create a workspace so that npm can manage both packages. In the root of the project, create a package.json file with the following content:

{
"private": true,
"workspaces": [
"example-app",
"my-plugin-module"
]
}

Now go to the example-app directory and execute the following command:

Terminal window
$ npm install my-plugin-module

If you look at the contents of example-app/package.json, you will see that a new dependency has been added to my-plugin-module in version 0.0.1. This information has been extracted from the my-plugin-module/package.json file. When creating the workspace, when we run npm install on one of the packages belonging to the workspace, requesting a dependency on another package in the workspace, npm will look for it in the workspace and not in the npm registry. The advantage of this is that it is not necessary to publish the package in the npm registry in order to use it. Also, any changes we make to the package will be immediately reflected in the other package.

Use the plugin module from the example application

Section titled “Use the plugin module from the example application”

Now we can use the plugin module in the example application. To do this, we will modify the example-app/main.js file to import the library and call the two functions we have defined:

example-app/main.js

import { Paella } from '@asicupv/paella-core';
import { sayHello, sayGoodbye } from "my-plugin-module";
import "@asicupv/paella-core/paella-core.css";
sayHello();
sayGoodbye();
...

Now, if you run the example application, you will see the following messages in the console:

Terminal window
Hello from my plugin!
Goodbye from my plugin!

With this we can work, but we can still improve the debugging environment a little bit. Add the following script in the package.json file from the root package, where the workspace is defined:

{
"scripts": {
...
"dev": "npm run -w my-plugin-module dev & npm run -w example-app dev",
}
}

To make the above script work, one more thing still needs to be modified. By default, Vite runs the development server outside the filesystem, so running npm run dev in the plugin module package will not modify the library distribution file. To fix this, you have to modify the dev script in the package.json of the plugin module:

my-plugin-module/package.json

{
"scripts": {
...
"dev": "vite build --watch"
}
}

Now you can run the following command in the root directory of the project:

Terminal window
$ npm run dev

Check the navigator console to see the messages from the plugin module. Now, you can modify the plugin module code and see how the changes are reflected in the example application:

my-plugin-module/src/main.js

export function sayHello() {
console.log("Hello from my updated plugin!");
}
...

When you save the changes, the library will be recompiled, the example application will use the new version of the library, and the browser will automatically reload the page. You will see the following message in the console:

Terminal window
Hello from my updated plugin!

So far we have created an npm package containing vanilla JavaScript code to test the npm workspaces system. Now we are going to create a module for paella-core with an example plugin. We need two things:

  • A plugin module file, which is a class that extends the PluginModule class. This class is used to group the plugins of the module and to provide some metadata about the module.
  • One or more plugin files.

To create the plugin, we need to have @asicupv/paella-core installed in the plugin package. Run the following command in the my-plugin-module directory:

Terminal window
$ npm install @asicupv/paella-core

Add an ExamplePluginModule.js file in the src directory of the plugin package with the following content:

my-plugin-module/src/ExamplePluginModule.js

import { PluginModule } from "@asicupv/paella-core";
import packageData from "../package.json";
let g_pluginModule = null;
export default class ExamplePluginsModule extends PluginModule {
static Get() {
if (!g_pluginModule) {
g_pluginModule = new ExamplePluginsModule();
}
return g_pluginModule;
}
get moduleName() {
return "example-plugin-module";
}
get moduleVersion() {
return packageData.version;
}
}

The moduleName and moduleVersion properties are mandatory. To get the version, we load the package.json file of the plugin and read the version property.

There are more properties that can be implemented, for more information see the documentation on plugin modules.

We are going to create a single button plugin that will be added to the player toolbar. Add the following file in the src directory of the plugin package:

my-plugin-module/src/es.upv.paella.exampleButtonPlugin.js

import {
ButtonPlugin
} from '@asicupv/paella-core';
import ExamplePluginModule from "./ExamplePluginModule";
const icon = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" width="24" height="24" stroke-width="2" style="fill: none;">
<path d="M18 4a3 3 0 0 1 3 3v8a3 3 0 0 1 -3 3h-5l-5 3v-3h-2a3 3 0 0 1 -3 -3v-8a3 3 0 0 1 3 -3h12z"></path>
<path d="M12 8v3"></path>
<path d="M12 14v.01"></path>
</svg>`;
export default class ExampleButtonPlugin extends ButtonPlugin {
getPluginModuleInstance() {
return ExamplePluginModule.Get();
}
get name() {
return "es.upv.paella.exampleButtonPlugin";
}
async load() {
this.icon = icon;
}
async action() {
alert("Hello from the example button plugin!");
}
}

As you can see, the instance of ExamplePluginModule is a singleton. This is done so that if we add more plugins to the module, all of them share the same instance of ExamplePluginModule. It is very important that the getPluginModuleInstance() function of all plugins in the same module return the same instance. The implementation can be done by a singleton, as in the example you see, or you can do it with any other method, as long as all plugins of the same module return the same instance.

To finish our plugin module we only need to export it in the main file of the library. By convention, plugins are exported in two ways:

  • The class of each plugin is exported.
  • An array is exported with all the plugins of the module with a default configuration.

When adding the plugin to the player, we can choose to add a single plugin, or add all the plugins of the module. In the first case, the plugin is added by importing the plugin class, and in the second case, the array with all the plugins of the module is added.

my-plugin-module/src/main.js

import ExampleButtonPlugin from "./es.upv.paella.exampleButtonPlugin";
export { ExampleButtonPlugin };
export const examplePlugins = [
{
plugin: ExampleButtonPlugin,
config: {
enabled: true,
side: "right"
}
}
]

Now we can import the plugin in our example application:

example-app/main.js

import { Paella } from '@asicupv/paella-core';
import { examplePlugins } from "my-plugin-module";
import "@asicupv/paella-core/paella-core.css";
const player = new Paella('playerContainer', {
plugins: [
...examplePlugins
]
});
await player.loadManifest();

We have added the plugin using the examplePlugins array that we have exported in the plugin module. In this case, a default configuration is included for the plugin that we have left activated by default. In the player we will have a new button to the right of the playback bar with the plugin we just created.

We can also add the plugin by importing the class directly, but in that case we have to configure the plugin manually:

example-app/main.js

import { Paella } from '@asicupv/paella-core';
import { ExampleButtonPlugin } from "my-plugin-module";
import "@asicupv/paella-core/paella-core.css";
const player = new Paella('playerContainer', {
plugins: [
ExampleButtonPlugin
]
});
await player.loadManifest();

example-app/public/config/config.json

{
...
"plugins": {
...
"es.upv.paella.exampleButtonPlugin": {
"enabled": true,
"side": "right"
}
}
}

To publish the plugin module, you have to use the npm CLI command. You’ll need to have an npm account, and you must to choose a name for your package that is not already taken. In this case, we have used the name my-plugin-module, but you probably want to use a different name. The package name is configured in the name property of the package.json file in the plugin module.

To login to your npm account, run the following command:

Terminal window
$ npm login

You’ll be redirected to the npm website in your browser, and after that you will be redirected to the command line.

Once you are logged in, you can publish the package by running the following command:

Terminal window
$ npm publish

You can also use a scope for your package. In this case, the package will be published under the name @your-scope/my-plugin-module, and by default will be available only for you and the users you give access to. Note that to publish a private package you need to have a paid npm account. If you want to publish a public package, you can do it without a paid account adding the --access public option when publishing the package.

For more information about how to publish a package: