Apps and Plugins

Cantemo Apps allows anyone to extend the features and capabilities of Cantemo. We have enabled areas in Cantemo to be extended by implementing interfaces so that you can override or enhance the functionality of Cantemo to suit your needs. You create Apps by using the Plugin system.

../_images/plugin_searchresults.png ../_images/plugin_collections.png

The Cantemo plugin system is based upon a component architecture which creates functional interfaces and services which allow components to easily extend each others’ functionality. Each component can offer its own API by declaring extension points.

We have created extension points in Cantemo which are detailed in this documentation, we also create extension points in the theme to allow you to inject basic information and functionality into a page.

As well as this, Cantemo raises many events which can be used to trigger functionality decoupled from the main Cantemo code base. Signals are raised at key moments such as pre-saving of a record or post-saving, and Receivers are notified of such events to trigger extra functionality.

Creating Apps

Apps are created as Python modules (single .py files), or as Python packages (folders with __init__.py files) and are placed within the plugins/ directory. On startup Cantemo reads the plugin directory for modules and packages and loads them. Any code that is using Plugin Extensions will be placed into the PluginEnvironment.

It is important to note that the apps do have an impact on the running of the Cantemo framework, any code should raise the appropriate exceptions and die safely, otherwise the performance or running of Cantemo can be directly affected.

My First Cantemo App

Starting from Cantemo 2.0 there is a Cantemo App skeleton creator for easily creating Cantemo Apps. We will use this to create a basic App.

After logging into a system containing Cantemo we change to the directory that contains Cantemo and run a management command which creates the basic App skeleton from a template. We will call it myfirstportalapp:

cd /opt/cantemo/portal
./manage.py start_portal_app myfirstportalapp

Now there is a portal app in /opt/cantemo/portal/portal/plugins/myfirstportalapp

The app you created will contain the following files:

/opt/cantemo/portal/portal/plugins/myfirstportalapp
/opt/cantemo/portal/portal/plugins/myfirstportalapp/plugin.py
/opt/cantemo/portal/portal/plugins/myfirstportalapp/templates
/opt/cantemo/portal/portal/plugins/myfirstportalapp/templates/myfirstportalapp
/opt/cantemo/portal/portal/plugins/myfirstportalapp/templates/myfirstportalapp/navigation.html
/opt/cantemo/portal/portal/plugins/myfirstportalapp/templates/myfirstportalapp/index.html
/opt/cantemo/portal/portal/plugins/myfirstportalapp/urls.py
/opt/cantemo/portal/portal/plugins/myfirstportalapp/views.py
/opt/cantemo/portal/portal/plugins/myfirstportalapp/__init__.py
Now restart Cantemo Web::

systemctl restart portal-web.service

Now your app should be registered and the admin overview should look like this

../_images/myfirstapp.png

Now you may edit the source in /opt/cantemo/portal/portal/plugins/myfirstportalapp/ and start developing your app! You will see there are files there which use the plugin infrastructure to register the app, create some basic templates which add to the navigation and create a page.

Example code

As part of our commitment to developers we supply example code to github.

Please look at https://github.com/Cantemo

App Packaging

Simple Apps can be placed in a single file (python module) and placed within the plugins directory, and the plugin system will import each file and register the Plugin interface directly. More complex Apps, such as those adding functionality with additional pages can be placed in their own directory as a python package. The start_portal_app command creates an own directory for the basic files needed for a package.

The python package is also placed within the plugins directory, and must contain an __init__.py file which imports any module within the package so that the Plugin loader can register the plugin.

Both modules and packages can reference any library that the python library has access too as well as the Cantemo framework.

App Package Structure

The plugins will be stored in a directory called plugins, and under this each plugin will have its own directory.

  • plugins/

    • 050_plugin_name_example/

      • context_processors/

      • middleware_classes/

      • templates/

      • tests/

      • urls.py

      • models.py

    • 051_plugin_name_example/..

These directories can have a set structure so that the plugin architecture can find the plugins it needs for each section of the site.

The naming of the plugins will decide the order that they will be included.

App Registration

Optionally Apps can be registered so that they show up in the admin menu. This will allow the administrators to see what version of your App they are running, and for you to display your company name and optionally the website. For example in a plugins.py write the following:

import logging

from portal.pluginbase.core import Plugin, implements
from portal.generic.plugin_interfaces import IPluginURL, IPluginBlock, \
                                             IAppRegister

log = logging.getLogger(__name__)

class MyNewAppRegister(Plugin):

    implements(IAppRegister)

    def __init__(self):
        self.name = "My New App"
        self.plugin_guid = "1ff0ada4-a3a2-1aea-a7b1-6e5371975c56"
        log.debug('Registered My New App')

    def __call__(self):
        _app_dict = {
                'name': self.name,
                'version': versionnumber,
                'author': 'Codemill AB',
                'author_url': 'www.cantemo.com',
                'notes': 'Copyright 2012. All Rights Reserved'}
        return _app_dict

mynewappplugin = MyNewAppRegister()

This registers a new IAppRegister type plugin. It then returns information to the system. If you are copying the above then you want to change some of the information:

  • self.name - Change to the name of your plugin

  • self.plugin_guide - Change to a unique number.

  • version - The version number of this plugin

  • author - Your company name

  • author_url - Your website address.

  • notes - Any other text information you want to show up.

App Removal

If you are removing apps, make sure to also remove all the compiled .pyc files from the plugin directory.

Alternatively bundle a script which cleans the package away, removes the .pyc files and removes the database models (using database migrations.

App Database Integration

You can use the Django models functionality to create database tables for storing and retrieving data for you App. The Django documentation contains very good documentation on how to do this.

Database Migration

For Database migration you should use Django’s migration framework. Creating a migration is done using: manage.py makemigrations <app name> More information can be found here: https://docs.djangoproject.com/en/1.11/topics/migrations/

Apps and avoiding import errors

The Apps can be imported before the rest of the Cantemo code, therefore we need to put in place measures to stop any cyclic import problems from happen. It is recommended that imports are not put at the head of python modules, but imported within functions that need them:

from portal.pluginbase.core import *

from portal.utils.plugin_interfaces import IPluginBlock
from portal.generic.plugin_interfaces import ITemplateChooser

class MyBlockPlugIn(Plugin):
    """ PLUGIN EXAMPLE
        This plugin will return the first user in the system:
    """

    implements(IPluginBlock)

    def __init__(self):
        self.name = "myblock"

    def return_string(self, tagname, *args):
        from django.contrib.auth.models import User
        u = User.objects.get(pk=1)
        return {'guid':'9748872c-3310-11e1-9d7c-27a06e9d671c', \
                'template':u.username }

pluginblock1 = MyBlockPlugIn()

This App is potentially unsafe and might cause import errors:

from portal.pluginbase.core import *

from portal.utils.plugin_interfaces import IPluginBlock
from portal.generic.plugin_interfaces import ITemplateChooser
from django.contrib.auth.models import User

class MyBlockPlugIn(Plugin):
    """ PLUGIN EXAMPLE
        This plugin will return the first user in the system:
    """

    implements(IPluginBlock)

    def __init__(self):
        self.name = "myblock"

    def return_string(self, tagname, *args):
        u = User.objects.get(pk=1)
        return {'guid':'9748872c-3310-11e1-9d7c-27a06e9d671c', \
                'template':u.username }

pluginblock1 = MyBlockPlugIn()

Please make sure to follow that convention to make Apps import safely. A better version that checks for the existence of the user would be:

from portal.pluginbase.core import *

from portal.utils.plugin_interfaces import IPluginBlock
from portal.generic.plugin_interfaces import ITemplateChooser

class MyBlockPlugIn(Plugin):
    """ PLUGIN EXAMPLE
        This plugin will return the first user in the system:
    """

    implements(IPluginBlock)

    def __init__(self):
        self.name = "myblock"
        self.guid = "a85a9ec4-3310-11e1-92de-efb93402adcb"

    def return_string(self, tagname, *args):
        from django.contrib.auth.models import User
        try:
            u = User.objects.get(pk=1)

        except User.DoesNotExist:
            u = None

        return {'guid':self.guid, 'template': u}

pluginblock1 = MyBlockPlugIn()

Apps in other development languages.

This is out of scope for this document, but Python’s support for creating modules in other languages and having inter-operability is very good.

For extended system support please contact us for our integration with messages queues in particular Redis, which can be used by other services using other languages to provide extra functionality.

Item page

Item page tabs

The item page allows for plugins to add, remove and modify tabs on the item page.

A tab is defined by this data structure:

{
  key: string;
  label: string;
  order: number; // Optional. Determines in what order the tabs should be shown
  roles: string[]; // Optional. Required roles for tab to be visible
  renderPlugin?: (parent, item) => (void | () => void);
}

renderPlugin is the main function, responsible for rendering the tab contents. It will be called with two parameters; parent is the HTML element it should render into and item contains information about the current item, with the fields id and item_type.

renderPlugin can return a callback function, which will then be invoked when the tab is destroyed (eg user changes to another tab), use this to perform any cleanup (eg unregister event handlers).

There are three function available to manage the tabs:

cntmo.plugin.itemPageTabs.removeTab($TAB_ID);
cntmo.plugin.itemPageTabs.modifyTab($PARTIAL_TAB_DEF);
cntmo.plugin.itemPageTabs.addTab($TAB_DEF);

modifyTab works by updating an existing tab definition based on the key field.

Here is a complete example. First create a plugin that adds to the header_css_js block:

class ItemTabsPlugin(Plugin):

    implements(IPluginBlock)

    def __init__(self):
        self.name = "header_css_js"
        self.plugin_guid = "0818096f-1660-4ea9-9cd1-a405e16103a4"

    def return_string(self, tagname, *args):
        return {"guid": self.plugin_guid, "template": "html/item_tabs.html"}

The item_tabs.html file could then looks like this:

<script>
    // Remove the Versions tab
    cntmo.plugin.itemPageTabs.removeTab("versions");

    // Move the History tab to the end and change the label
    cntmo.plugin.itemPageTabs.modifyTab(
        { key: "history", order: 9999, label: "Old stuff" }
    );

    // Add a new "Example" tab
    cntmo.plugin.itemPageTabs.addTab({
        key: "example",
        label: "Example",
        // Required roles for this tab
        roles: ['portal_items_formats_read'],
        renderPlugin: (parent, item) => {
          // Render into the parent element.
          // We suggest you don't use innerHTML in real code, this is only for demonstration.
          parent.innerHTML = `Example <span style="color: red;">tab</span> for item ${item.id}`;
          return () => {
            console.log("Perform any cleanup here");
          };
        },
        order: 350 // After the Posters tab
    });
  </script>

Subclip page tabs

Similar to item page tab, plugins can control the tabs on the subclip page. There are only minor differences.

Use cntmo.plugin.subclipPageTabs instead of cntmo.plugin.itemPageTabs.

The renderPlugin function context data is different, it has a id and a parent_id field.

Here is example of a very simple subclip tab plugin:

cntmo.plugin.subclipPageTabs.addTab({
    key: "subclipexample",
    label: "Subclip Example",
    order: 450,
    renderPlugin: (parent, subclip) => {
      parent.innerText = `This is subclip ${subclip.id} of item ${subclip.parent_id}`;
    },
  }
)

Query params

The item page can load a specific time in the video. Specify the timecode in the startTimecode query param for the item page, eg:

/item/VX-87725/?startTimecode=00:01:02:03

Accurate.Video

The video player can be controlled via javascript in various ways, below are a few examples. Full documentation can be found at https://sdk.accurate.video/

Play and pause the video

The player can be accessed through the window.player object:

window.player.api.play();
window.player.api.pause().then(() => {
    console.log("Paused at", window.player.api.getCurrentTime());
});

These and many other functions are async, and return a promise that resolves when the action is complete.

Jump to specific frame

Use the seek function to seek in the video, this seeks to frame 123:

window.player.api
  .seek({
    format: 1,
    time: 123,
    mode: 2,
  })
  .then(() => { console.log("Seek complete"); });

format specifies the format of the time field, other useful values include 0 for timecode (eg 00:01:23:21) and 3 for seconds (eg 12.54).

Get and set in- and out-point

The in- and out-point can be can also be accessed and updated through the point plugin:

console.log(window.player.api.plugin.apPointPlugin.points);

const frameRate = { numerator: 25, denominator: 1 };
window.player.api.plugin.apPointPlugin.setInPointFrame(
    123, frameRate
);
window.player.api.plugin.apPointPlugin.setOutPointFrame(
    321, frameRate
);

Naming of subtitle, video, and audio tracks (components)

Accurate.Player supports multiple audio tracks if the tracks are imported as separate shapes.

Subtitle, video, and audio tracks can be assigned a human readable label that is displayed in the video player. The track label is assigned by setting a metadata field on the corresponding component in Vidispine.

Example of how to set the track label programmatically for component VX-2317, of shape VX-2262, in item VX-2176:

[root@portal]# /opt/cantemo/portal/manage.py shell_plus
...
>>> from portal.vidispine.iitem import ItemHelper
>>> ith = ItemHelper()
>>> ith.setComponentMetadata('VX-2176', 'VX-2262', 'VX-2317', 'title', 'Swedish')

Example of how to set the same label using cURL:

[root@portal]# curl -u admin:admin -H 'Content-Type: text/plain' -X PUT 'http://localhost:8080/API/item/VX-2176/shape/VX-2262/component/VX-2317/metadata/title' -d 'Swedish'

Development Reference

More information on different areas of Cantemo for development.

Vidispine Integration