Some questions concerning module development

Using Xibo 1.8.9 under docker

I’m trying to customize a video widget to show an html title above it in it’s contained region. Basically adding an image title in html, to match our existing layout from another signage software.

I read the docs, https://xibo.org.uk/manual/en/advanced_modules.html and other posts to help me figure out what was needed and where

Since the current video module doesn’t allow you to add custom HTML or CSS, I thought creating my own would be easy. Same twig files for this new module, almost same code except for the getRessource to render the html and voilà.

But no.

If I set the module to be regionSpecific, it raises an error when addding a widget from this module because no media are assigned to it at first :

An exception has been thrown during the rendering of a template ("No file to return").
#0 /var/www/cms/cache/ae/ae1fe01a0a7a5af7485225790a509767c029f5f7f4b7402cd948bcad760b42b6.php(75): Twig_Template->displayBlock('formHtml', Array, Array) 
#1 /var/www/cms/vendor/twig/twig/lib/Twig/Template.php(215): __TwigTemplate_707e05763b5ad99e40a51a7d48fbad2f482756c82a210020828a39fbdd9f00be->block___internal_f313ffacb312194ed23810ebbdecab442b2114a9ff3d4e76067d65ff9016ff38(Array, Array) 
#2 /var/www/cms/vendor/twig/twig/lib/Twig/Template.php(279): Twig_Template->displayBlock('__internal_f313...', Array, Array, true) 
#3 /var/www/cms/cache/ae/ae1fe01a0a7a5af7485225790a509767c029f5f7f4b7402cd948bcad760b42b6.php(37): Twig_Template->renderBlock('__internal_f313...', Array, Array) 
#4 /var/www/cms/vendor/twig/twig/lib/Twig/Template.php(432): __TwigTemplate_707e05763b5ad99e40a51a7d48fbad2f482756c82a210020828a39fbdd9f00be->doDisplay(Array, Array) 
#5 /var/www/cms/vendor/twig/twig/lib/Twig/Template.php(403): Twig_Template->displayWithErrorHandling(Array, Array) 
#6 /var/www/cms/cache/d9/d9a0be7befa7d7655bed656724d72dfe030b2b6de47ea04bd467d57b31d9328b.php(31): Twig_Template->display(Array, Array) 
#7 /var/www/cms/vendor/twig/twig/lib/Twig/Template.php(432): __TwigTemplate_576d02ca6d755ceeba5dc763ea2b3554215869a4996f7ea45be2f9fac1427e31->doDisplay(Array, Array) 
#8 /var/www/cms/vendor/twig/twig/lib/Twig/Template.php(403): Twig_Template->displayWithErrorHandling(Array, Array) 
#9 /var/www/cms/vendor/twig/twig/lib/Twig/Template.php(411): Twig_Template->display(Array) 
#10 /var/www/cms/vendor/slim/views/Twig.php(91): Twig_Template->render(Array) 
#11 /var/www/cms/lib/Controller/Base.php(410): Slim\Views\Twig->render('etsvideo-form-a...', Array) 
#12 /var/www/cms/lib/Controller/Base.php(323): Xibo\Controller\Base->renderTwigAjaxReturn(Array, Object(RKA\Slim), Object(Xibo\Helper\ApplicationState)) 
#13 /var/www/cms/lib/Middleware/State.php(117): Xibo\Controller\Base->render() 
#14 [internal function]: Xibo\Middleware\State->Xibo\Middleware\{closure}() 
#15 /var/www/cms/vendor/slim/slim/Slim/Slim.php(1208): call_user_func_array(Object(Closure), Array) 
#16 /var/www/cms/vendor/slim/slim/Slim/Slim.php(1356): Slim\Slim->applyHook('slim.after.disp...') 
#17 /var/www/cms/vendor/slim/slim/Slim/Middleware/Flash.php(85): Slim\Slim->call() 
#18 /var/www/cms/vendor/slim/slim/Slim/Middleware/MethodOverride.php(92): Slim\Middleware\Flash->call() 
#19 /var/www/cms/lib/Middleware/Actions.php(160): Slim\Middleware\MethodOverride->call() 
#20 /var/www/cms/lib/Middleware/Theme.php(36): Xibo\Middleware\Actions->call() 
#21 /var/www/cms/lib/Middleware/WebAuthentication.php(131): Xibo\Middleware\Theme->call() 
#22 /var/www/cms/lib/Middleware/CsrfGuard.php(63): Xibo\Middleware\WebAuthentication->call() 
#23 /var/www/cms/lib/Middleware/State.php(121): Xibo\Middleware\CsrfGuard->call() 
#24 /var/www/cms/lib/Middleware/Storage.php(47): Xibo\Middleware\State->call() 
#25 /var/www/cms/lib/Middleware/Xmr.php(37): Xibo\Middleware\Storage->call() 
#26 /var/www/cms/vendor/slim/slim/Slim/Slim.php(1300): Xibo\Middleware\Xmr->call() 
#27 /var/www/cms/web/index.php(124): Slim\Slim->run() 
#28 {main}

If I set the module to be not regionSpecific, it uses the LibraryManager form to assign a media item to it, and creates a module video and not my own module.

Crawling through the code, I see that the module created from the library is determined by the media extensions of enabled modules. Ok, i’ll just disable the the video module then since I only need my own, but now when I try to add my module and assign a video media to it through the library,

  • It still creates the default video module despite being disabled
  • Media Library can’t filter video type (extension is disabled) and my module doesn’t list any mpeg/mp4 files or any other extensions (I assigned the same valid extensions than the video )

Also - worth mentionning, I kept getting twig error message at first when I tried to add a widget from my module with regionSpecific enabled. The message "No file was found’ was confusing since I thought it couldn’t find a twig template when the exception originated from having no mediaID when trying to show the custom video-add-form.

10783 2018-08-22 20:53 WEB GET DEBUG /playlist/widget/form/edit/28 Getting first primary media for Widget: 28 Media: [] audio []

Code

custom\ETSVideo.json

    {
    "title": "ETS video",
    "author": "Ecole de technologie supérieure - Jean-Sebastien Gervais",
    "description": "Module vidéo a l'intérieur du gabarit ETS avec titre",
    "name": "ETSVideo",
    "class": "Xibo\\Custom\\ETSVideo\\ETSVideo"
    }

custom\ETSvideo\ETSvideo.php

<?php
/*
 * École de technologie supérieure
 * Jean-Sébastien Gervais
 *
 */


namespace Xibo\Custom\ETSVideo;


use Respect\Validation\Validator as v;
use Xibo\Exception\InvalidArgumentException;
use Xibo\Exception\XiboException;
use Xibo\Factory\ModuleFactory;
use Xibo\Widget\ModuleWidget;

/**
 * Class Hls
 * @package Xibo\Custom
 */
class ETSVideo extends ModuleWidget
{

    public $codeSchemaVersion = 1;

    /**
     * Form for updating the module settings
     */
    public function settingsForm()
    {
        // Return the name of the TWIG file to render the settings form
        return 'ETSVideo-form-settings';
    }

    /**
     * Process any module settings
     * TODO
     */
    public function settings()
    {
        // Process any module settings you asked for.
        $this->module->settings['defaultMute'] = $this->getSanitizer()->getCheckbox('defaultMute');

        if ($this->getModule()->defaultDuration !== 0)
            throw new \InvalidArgumentException(__('The Video Module must have a default duration of 0 to detect the end of videos.'));

        // Return an array of the processed settings.
        return $this->module->settings;
    }



    /** @inheritdoc
    public function init()
    {
        // Initialise extra validation rules
        v::with('Xibo\\Validation\\Rules\\');
    }
     */

    /**
     * Install or Update this module
     * @param ModuleFactory $moduleFactory
     */
    public function installOrUpdate($moduleFactory)
    {
        if ($this->module == null) {
            // Install
            $module = $moduleFactory->createEmpty();
            $module->name = 'ETSVideo';
            $module->type = 'ETSVideo';
            $module->class = 'Xibo\Custom\ETSVideo\ETSVideo';
            $module->description = 'Module vidéo, gabarit ÉTS avec titre "Youtube"';
            $module->imageUri = 'forms/library.gif';
            $module->enabled = 1;
            $module->previewEnabled = 1;
            $module->assignable = 1;
            $module->regionSpecific = 0;
            $module->renderAs = 'html';
            $module->schemaVersion = $this->codeSchemaVersion;
            $module->defaultDuration = 60;
            $module->settings = [];
            $module->viewPath = '../custom/ETSVideo';
            
            $this->setModule($module);
            $this->installModule();
        }

        // Check we are all installed
        $this->installFiles();
    }

    /**
     * Install Files
     */
    public function installFiles()
    {
        $this->mediaFactory->createModuleSystemFile(PROJECT_ROOT . '/modules/vendor/jquery-1.11.1.min.js')->save();
        $this->mediaFactory->createModuleSystemFile(PROJECT_ROOT . '/modules/xibo-layout-scaler.js')->save();
        
        // Install files from a folder
        $folder = PROJECT_ROOT . '/custom/ETSVideo/resources';
        foreach ($this->mediaFactory->createModuleFileFromFolder($folder) as $media) { 
            $media->save();
        }
  
    }


    



    /**
     * Override previewAsClient
     * @param float $width
     * @param float $height
     * @param int $scaleOverride
     * @return string
     */
    public function previewAsClient($width, $height, $scaleOverride = 0)
    {
        return $this->previewIcon();
    }

    /**
     * Determine duration
     * @param $fileName
     * @return int
     */
    public function determineDuration($fileName = null)
    {
        // If we don't have a file name, then we use the default duration of 0 (end-detect)
        if ($fileName === null)
            return 0;

        $this->getLog()->debug('Determine Duration from %s', $fileName);
        $info = new \getID3();
        $file = $info->analyze($fileName);
        return intval($this->getSanitizer()->getDouble('playtime_seconds', 0, $file));
    }



    /**
     * Template for Layout Designer JavaScript  (adds javascript in module add/edit)
     * @return string
     */
    //public function layoutDesignerJavaScript()
    //{
    //  return 'my-module-javascript';
    //}
 
    public function add()
    {
        //$this->setCommonOptions();
        //$this->validate();

        // Save the widget
        $this->saveWidget();
    }

    /**
     * Edit Media
     */
    public function edit()
    {
        $this->setCommonOptions();
        $this->validate();

        // Save the widget
        $this->saveWidget();
    }
    /**
     * Set common options
     */
    private function setCommonOptions()
    {
        $this->setDuration($this->getSanitizer()->getInt('duration', $this->getDuration()));
        $this->setUseDuration($this->getSanitizer()->getCheckbox('useDuration'));
        $this->setOption('name', $this->getSanitizer()->getString('name'));
        $this->setOption('uri', urlencode($this->getSanitizer()->getString('uri')));
        $this->setOption('mute', $this->getSanitizer()->getCheckbox('mute'));

        // Only loop if the duration is > 0
        if ($this->getUseDuration() == 0 || $this->getDuration() == 0)
            $this->setOption('loop', 0);
        else
            $this->setOption('loop', $this->getSanitizer()->getCheckbox('loop'));

        // This causes some android devices to switch to a hardware accellerated web view
        $this->setOption('transparency', 0);
    }

    /**
     * Validate
     * @throws XiboException
     */
    private function validate()
    {
        if ($this->getUseDuration() == 1 && $this->getDuration() == 0)
            throw new InvalidArgumentException(__('Please enter a duration'), 'duration');

        if (!v::url()->notEmpty()->validate(urldecode($this->getOption('uri'))))
            throw new InvalidArgumentException(__('Please enter a link'), 'uri');
    }


    /**
     * Set default widget options
     */
    public function setDefaultWidgetOptions()
    {
        parent::setDefaultWidgetOptions();
        $this->setOption('mute', $this->getSetting('defaultMute', 0));
    }

    /**
     * @inheritdoc
     */
    public function isValid()
    {
        // Using the information you have in your module calculate whether it is valid or not.
        // 0 = Invalid
        // 1 = Valid
        // 2 = Unknown
        return 1;
    }

    /**
     * GetResource
     * Return the rendered resource to be used by the client (or a preview) for displaying this content.
     * @param integer $displayId If this comes from a real client, this will be the display id.
     * @return mixed
     */
    public function getResource($displayId = 0)
    {
        // Ensure we have the necessary files linked up
        $media = $this->mediaFactory->createModuleFile(PROJECT_ROOT . '/modules/vendor/hls/hls.min.js');
        $media->save();
        $this->assignMedia($media->mediaId);

        $this->setOption('hlsId', $media->mediaId);

        $media = $this->mediaFactory->createModuleFile(PROJECT_ROOT . '/modules/vendor/hls/hls-1px-transparent.png');
        $media->save();
        $this->assignMedia($media->mediaId);

        $this->setOption('posterId', $media->mediaId);
        
        $arrow  = $this->getResourceUrl('ets_arrow.png');
        $ytlogo = $this->getResourceUrl('youtube-logo.png');

        // Render and output HTML
        $this
            ->initialiseGetResource()
            ->appendViewPortWidth($this->region->width)
            ->appendJavaScriptFile('vendor/jquery-1.11.1.min.js')
            ->appendJavaScriptFile('vendor/hls/hls.min.js')
            ->appendJavaScript('
                $(document).ready(function() {
            
                    if(Hls.isSupported()) {
                        var video = document.getElementById("video");
                        var hls = new Hls({
                            autoStartLoad: true,
                            startPosition : -1,
                            capLevelToPlayerSize: false,
                            debug: false,
                            defaultAudioCodec: undefined,
                            enableWorker: true
                        });
                        hls.loadSource("' . urldecode($this->getOption('uri')) . '");
                        hls.attachMedia(video);
                        hls.on(Hls.Events.MANIFEST_PARSED, function() {
                          video.play();
                        });
                        hls.on(Hls.Events.ERROR, function (event, data) {
                            if (data.fatal) {
                                switch(data.type) {
                                    case Hls.ErrorTypes.NETWORK_ERROR:
                                        // try to recover network error
                                        //console.log("fatal network error encountered, try to recover");
                                        hls.startLoad();
                                        break;
                                    
                                    case Hls.ErrorTypes.MEDIA_ERROR:
                                        //console.log("fatal media error encountered, try to recover");
                                        hls.recoverMediaError();
                                        break;
                                        
                                    default:
                                        // cannot recover
                                        hls.destroy();
                                        break;
                                }
                            }
                        });
                     }
                });
            ')
            ->appendBody('
                <img src="' . $this->getResourceUrl('custom/ETSVideo/resources/ets_arrow.png') . '"/>
                <img src="' . $this->getResourceUrl('custom/ETSVideo/resources/youtube-logo.png') . '"/>
                <video id="video" poster="' . $this->getResourceUrl('vendor/hls/hls-1px-transparent.png') . '" ' . (($this->getOption('mute', 0) == 1) ? 'muted' : '') . '>
                </video>
            ')
            ->appendCss('
                video {
                    width: 100%; 
                    height: 100%;
                }
            ')
        ;

        return $this->finaliseGetResource();
    }

    /** @inheritdoc */
    public function getCacheDuration()
    {
 
        return 3600;
    }
}

custom\ETSvideo\etsvideo-form-add.twig
custom\ETSvideo\etsvideo-form-edit.twig
custom\ETSvideo\etsvideo-form-settings.twig

same as the default video***.twig module in modules\ folder

So my question is :

Is it possible to override the default video module to use your own? or is there something I missed ?