Xibo Training Video Series

One often-requested thing over on the Xibo Community Site is some simple videos to give new users a start in using the Xibo system.

We’ve put together initially 4 videos that walk new users through creating basic layouts, the reasoning behind media durations and media management. We hope that they’re useful to you!

We’ll be taking feedback for these on the associated post over on the Community Site with a view to producing a longer series in conjunction with the 1.8 series stable release if they prove popular.

Using DataSets to display meeting room bookings

DataSets are remarkably flexible, yet if you’ve not used them before they can appear a little daunting.

In this article we’ll walk through producing a layout to show the current meeting in a meeting room, that will update automatically with information from a DataSet. The finished layout will look like this.

Xibo Meeting Room Sign

So to begin with, you need to be using Xibo CMS 1.6.1 or later. Versions earlier than that do have DataSets available, but not all of the features we’ll be using are in those earlier builds.

So to begin, log in to your CMS and go to Library -> DataSets.

Click the Add DataSet button and fill in the form. I’ve called my DataSet “Meeting Room 1” to represent that this DataSet will hold the schedule for that meeting room.

Add DataSet

We now need to define the fields that the DataSet will hold. For any given meeting, we need to know:

  • When it starts
  • When it finishes
  • Meeting Name
  • Meeting Description/Notes

So we’ll create 4 columns in the DataSet as follows:

  • dtMeetingStart
  • dtMeetingFinish
  • sMeetingName
  • sDescription

The order of the columns doesn’t matter in this example, since we won’t be showing the DataSet in a tabular format later on, but if you did want to also show a complete agenda for the day, then it’s best to have the columns in the order you’d like them to appear in that table.

So next to your Meeting Room 1 DataSet, click the Actions Menu, then View Columns. One column (Col1) is added automatically for you.

DataSet Columns

Click Action next to Col1 then Edit to edit that column.

Column Edit

Change Heading to be dtMeetingStart, Column Type should be set to Value, Data Type should be set to String, Column Order should be set to 1, the other values can stay blank. Click Save to save your changes.

Now add the additional columns to the Dataset, increasing Column Order by 1 for each column. When you’ve finished, your DataSet should look like this.

DataSet Columns Complete

You can then click on Close to close the Column Edit dialog.

Next we need to add some data in to our DataSet. Initially we’ll manually key data in, but you could choose to import it direct from a CSV file which could have come from another system such as a meeting room booking system.

So from the DataSets Library page, click the Action button next to your Meeting Room 1 DataSet and choose View Data. You’ll be presented with a tabular view of your DataSet. Enter in some test data. Ensure one meeting would be running at the time you’re going to test the layout (so there’s something to see later!).

The date must be expressed in MySQL date format. So that’s YYYY-MM-DD HH:MM:SS

Edit Data

The edits you make save automatically as you go. There’s no “Save” button to press.

When you’ve got some test data in the DataSet, it’s time to build a layout to leverage that DataSet and show the meeting room bookings!

For the purposes of this tutorial I’ve downloaded a background image from the Xibo Layout Exchange called Blue Green Clock – but you could use any other artwork or a plain background.

Create a new Layout and add the background image or colour of your choice. Ensure there is one region and that it’s positioned in your layout wherever you want the meetings to appear.

Next edit the region timeline for your region and add a new Ticker. Tickers can take their source from an RSS feed, but can now also take a feed from a DataSet. So we’ll set this Ticker to use our Meeting Room 1 DataSet.

Create Ticker

I’ve chosen a duration of 90 seconds but you can set a value appropriate to the layout you’re creating.

You’ll then be taken to the Ticker edit screen

Ticker Edit

First choose “Single” from the Direction select box. That means “Show one result at a time”. Update Interval is how long the client should wait between checks to make sure there’s no new data in the DataSet. For a live booking system, you might want to consider using a value of 1-5 minutes there rather than the default of 120 minutes. Lower Row and Upper Row limits need to be set to 0. Items per page needs to be set to 0. Filter is where we’ll control showing only the current meeting. It’s set to “NOW() > dtMeetingStart AND NOW() < dtMeetingFinish” which means only show items where the meeting has started, but hasn’t finished yet.

Finally we need to layout the text fields from the Dataset to make an attractive layout. You can double click on the text items under Available Substitutions to add them to the layout. When you’re done in should look something like this.

Layout Edit

Finally click Save to save your changes.

Assuming there’s a current meeting running based on the data in your DataSet, you should now see that meeting’s information in the layout designer.

Layout Design Preview

If you then schedule the layout to run on a client, or via the Layout Preview button, you’ll see the correct data displayed for the meetings in the room, according to the schedule in the DataSet. If you edit the data in the DataSet, the changes will be available in the system straight away, and the clients will pick up those changes on their next collection.

Scripting Xibo Content Management – A brief tour of the API

One common use case people put forward is that they want to update a particular piece of content in a layout automatically on a schedule.

That could take the form of a video being updated on a daily basis, or an image.

Since Xibo 1.5 series, there has been a “work in progress” OAuth authenticated API for scripting actions in the Xibo CMS. As of Xibo 1.6.0-rc1, there are enough API methods implemented to do some basic layout and media management to script tasks such as these.

This article is a basic walk through showing you how to connect to the API, authorise an application to use the Xibo CMS on your behalf, a quick look at some basic API methods, and finally complete code to automatically replace a media item in a layout.

Firstly you must be running Xibo CMS 1.6.0-rc1 or later. Earlier versions may not have the API methods required for this guide.

This guide uses Python as the scripting language. Python is chosen because it’s cross platform, widely available, and it’s what the Xibo project uses to test the API with, so there are existing libraries (XiboAPI) that we can leverage. It’s perfectly possible to connect to the API from other languages. Indeed some example PHP code is provided in the main Xibo repository. The use of the API from other languages however falls outside this scope of this guide.

Most modern Linux installations will come with Python ready-installed. Windows users can download and install Python here: http://www.python.org/getit This guide assumes you have Python 2.7 installed.

  • You also need the Python OAuth2 library installed.
    • On Linux, you’ll need to install the “python-oauth2” package (at least on Debian based systems such as Ubuntu).
    • On Windows, the process is slightly less straight forward
      • Download OAuth2 library from github: https://github.com/simplegeo/python-oauth2/archive/master.zip
      • Extract the downloaded file somewhere on your PC (remember where this is!)
      • Open a command promt (Start -> Run -> cmd)
      • Change directory to where you extracted OAuth2 to (eg cd c:\tmp\python-oauth2-master)
      • Then run: python setup.py install
      • The OAuth2 library will be installed to your Python installation
  • Make an empty folder somewhere to store your new script in.
  • Download the XiboAPI.py library and config file, and save a copy of both in your new folder.
  • Python is a white-space delimited language, which means that the amount each line of code is indented defines which code block a line belongs to. It’s therefore very important that you setup your text editor to follow the standard Python convention. Tab characters should not be used, but instead four space characters. On Linux, gedit is a suitable choice, and on Windows I’d recommend Notepad++.
  • This guide will give you the overview of how to communicate with the Xibo CMS via the API. It is not intended to be a guide on programming in the Python scripting language. There’s a plethora of resources out there aimed at that. I would recommend Dive into Python as a good primer before embarking on this is Python is your chosen language.
  • So lets make a start! We’ll create a new file called ReplaceVideo.py with the following contents:
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Script to upload a Video to the Xibo CMS and replace
# an old copy of it in a region

# Imports
import os
from xml.dom import minidom
import XiboAPI

api = XiboAPI.XiboAPI()
  • So there we’ve defined that the script is Python and that it’s UTF-8 encoded (so we don’t have issues with unicode characters down the line). We then import the os, minidom and XiboAPI libraries. Finally we create an instance of the XiboAPI to communicate with the CMS.
  • Next we need to register your application with the Xibo CMS. You’ll need an administrative login to your Xibo CMS.
    • Once logged in as an administrator (eg xibo_admin), go to Administration -> Applications
    • Click Add Application
    • Enter your name and email address. You can leave the third and fourth boxes (website and callback URL empty)
    • Click Register
    • Note the Key and Secret that have been generated. We’ll need those in the next step.
  • Next open defaults.cfg in your text editor. We need to point the XiboAPI library at your Xibo CMS.
  • Edit the file as follows, replacing the consumer_key and consumer_secrets with the values you got from your CMS, and the URL for your Xibo CMS:
[Main]
secret = NULL
token = NULL
url = http://yourXiboServer.com/path/xibo
consumer_key = 1b9e2c84fc747257375ae3eaaa01d49705303dac7
consumer_secret = 948989ef9eb9c17c26369a2265c35cea
  • Save defaults.cfg and close it
  • Save your ReplaceVideo,py script
  • Now lets run what we have:
alex@alex-laptop:~/scratch/xibo-release/ReplaceVideo$ python ReplaceVideo.py
Request Token:
 - oauth_token = 7dda5851bf99e09bdad1b27aebaa6fd005303dc3e
 - oauth_token_secret = a289a41cab324475c849283d553b113b

Go to the following link in your browser:
http://localhost/html/xibo-16/server/index.php?p=oauth&q=authorize&oauth_token=7dda5851bf99e09bdad1b27aebaa6fd005303dc3e

Have you authorized me? (y/n)
  • That’s the output from my script so far. So hopefully your script will attempt to connect to the Xibo CMS at the URL you configured, and will ask you to authorise it with the CMS. Now before you click the link, it’s important that you log on to the Xibo CMS as the user you want the script to impersonate. So if the script needs to appear to be the user “alex”, log off the Xibo CMS and log in as “alex”. Then follow the link.
  • You’ll be taken to a page “Xibo API – Authorization Requested”. Click the Allow button. Since we didn’t define a callback URL, the page will refresh lots now. That’s normal and can be ignored. Keep clicking the Allow button until you see a message ‘Request authorized. Please return to your application.’
  • Go back to your running script and enter “y” to indicate the API request has been authorised.
  • You will see that access has been authorised:
Access Token:
 - oauth_token = 93399a91be50497afd0e3f66521ddc4b05303de08
 - oauth_token_secret = 104e4911f5f0e1dc9a45978d2428cbfa

You may now access protected resources using the access tokens above.
  • Excellent, so now we have access to the XiboAPI as your intended user. I appreciate that’s alot of setup work but most of this only ever needs to be done once. You can reuse the same XiboAPI object in more than one project.
  • Your access tokens will automatically be written to disk in a file called site.cfg. Keep that file safe as the contents allow API access to your Xibo CMS.
  • Now let’s do something useful with the API! Lets get a list of layouts in the system. Add the following to your ReplaceVideo.py script:
# Call the LayoutList Method
response, status, code, message, content = api.callMethod('LayoutList')

# Parse the response XML
doc = minidom.parseString(content)

#List the layouts
nodes = doc.getElementsByTagName('layout')

for layout in nodes:
    layoutId = int(layout.attributes['layoutid'].value)
    layoutName = str(layout.attributes['layout'].value)
    layoutDescription = str(layout.attributes['description'].value)
    layoutTags = str(layout.attributes['tags'].value)

    print "Layout ID %d" % layoutId
    print "    Name: %s" % layoutName
    print "    Description: %s" % layoutDescription
    print "    Tags: %s\n" % layoutTags

print "Layout List Complete\n\n"
  • Save the file and run it again. Note this time since we already have API credentials, the access to the API is seemless and no user interaction is required. A list of layouts the user has access to view is shown:
alex@alex-laptop:~/scratch/xibo-release/ReplaceVideo$ python ReplaceVideo.py 
Layout ID 4
    Name: Default Layout
    Description: 
    Tags: 

Layout ID 5
    Name: My Video Layout
    Description: Daily Updated Video
    Tags: video 

Layout List Complete
  • So here the Layout we’re interested in is Layout ID 5. Lets add some code to see what regions are in Layout ID 5. Add the following to ReplaceVideo.py:
# We're interested in LayoutID 5
params = [('layoutid', '5')]

# Call the RegionList Method
response, status, code, message, content = api.callMethod('LayoutRegionList', params)

# Parse the response XML
doc = minidom.parseString(content)

# List the Regions
nodes = doc.getElementsByTagName('region')

for node in nodes:
    regionId = str(node.attributes['regionid'].value)
    regionWidth = int(node.attributes['width'].value)
    regionHeight = int(node.attributes['height'].value)
    regionTop = int(node.attributes['top'].value)
    regionLeft = int(node.attributes['left'].value)

    print "Region ID %s" % regionId
    print "    Width: %s" % regionWidth
    print "    Height: %s" % regionHeight
    print "    Top: %s" % regionTop
    print "    Left: %s\n" % regionLeft

print "Region List Complete\n\n"
  • Again save the file and run it again:
Region ID 47ff29524ce1b
    Width: 409
    Height: 450
    Top: 0
    Left: 0

Region ID 5303e4c0c0973
    Width: 227
    Height: 212
    Top: 40
    Left: 502

Region List Complete
  • So my example layout (id 5) has two regions on it. The right-most region is the one which will host our video and that’s the second one in the list (we know it’s that one as the Left value is highest putting it further away from the left hand edge of the layout). So we’ll make a note of the region ID (5303e4c0c0973)
  • Now let’s see what’s in that region already:
# We're interested in LayoutID 5
# RegionID 5303e4c0c0973
params = [('layoutid', '5'),
          ('regionid', '5303e4c0c0973')]

# Call the RegionList Method
response, status, code, message, content = api.callMethod('LayoutRegionTimelineList', params)

# Parse the response XML
doc = minidom.parseString(content)

# List the Regions
nodes = doc.getElementsByTagName('media')

for node in nodes:
    mediaId = str(node.attributes['mediaid'].value)
    mediaType = str(node.attributes['mediatype'].value)
    mediaDuration = str(node.attributes['mediaduration'].value)

    print "Media ID %s" % mediaId
    print "    Type: %s" % mediaType
    print "    Duration: %s\n" % mediaDuration

print "Media List Complete\n\n"
  • Now save ReplaceMedia.py and run it:
Media ID 11
    Type: video
    Duration: 0

Media List Complete
  • See the video that’s already in the region listed.
  • Hopefully that’s given a basic overview of how to navigate the API, how to pass in parameters etc. Now we’ll remove most of the code we’ve written and get on with the job in hand of scripting the file upload and replacing that video in the region. We need to remember it’s Layout ID 5, region 5303e4c0c0973 (or the correct values from your CMS). That’s how we’ll identify where we want our video to end up being added, and the old video to delete.
  • So now your ReplaceVideo.py should look like this:
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Script to upload a Video to the Xibo CMS and replace
# an old copy of it in a region

# Imports
import os
from xml.dom import minidom
import XiboAPI

api = XiboAPI.XiboAPI()

layoutId = 5
regionId = '5303e4c0c0973'
fileToUpload = 'myVideo.mp4'
mediaName = 'Daily Video'
  • Clearly set layoutId and regionId to the correct values we obtained earlier. Change fileToUpload to be the filename of the video you want to upload. It could be in the same directory as your script (in which case just the name is sufficient), or it could be a full path to the file. It’s up to you.
  • I’ve included some library functions to do the heavy lifting for you. Add the following to the file and save:
#################################
# START LIBRARY FUNCTIONS
#################################

def uploadFile(api,path,chunksize=256000,fileid='0',offset=0,size=0):
    # Uploads file at path (full path)
    # Returns a file upload ID which can be used to add the media
    # to the library
    #
    # eg uploadFile(api,'/home/user/1.jpg')
    #
    # Optional Parameters:
    #  chunksize - Int: How many bytes to upload at each pass
    #  fileid - String: The fileUploadID to use. Useful if you're resuming an interupted upload.
    #  offset - Long: How many bytes to skip over before beginning upload. Useful for resuming an interupted upload.
    #  size - Long: How many bytes to upload. 0 = the whole file. Useful to break an upload part way through for testing.
    #              Must be smaller than the size of the file in bytes.

    # First check the test file exists
    if not os.path.isfile(path):
        print "Error: File does not exist %s " % path
        exit(1)

    checksum = ''
    data = ''
    if size == 0:
        size = os.path.getsize(path)
    chunkOK = False

    # If the file is smaller than the chunksize, send one chunk
    # of the correct size.
    if chunksize > size:
        chunksize = size
 
    # Open the file for binary read
    fh = open(path,'rb')

    while offset < size:
        attempts = 0
        chunkOK = False
        while attempts < 3 and not chunkOK:
            attempts += 1

            # Read chunksize bytes of the file
            fh.seek(offset)
            data = fh.read(chunksize)

            # Data needs to be base64 encoded to be sent
            data, checksum = api.b64encode(data)

            params = [('fileId',fileid),
                      ('offset',offset),
                      ('checksum',checksum),
                      ('payload',data)
                     ]

            response, status, code, message, content = api.callMethod('LibraryMediaFileUpload', params)

            # If the chunk went up OK then move to the next chunk
            if status == 'OK':
                chunkOK = True
            else:
                print 'Uploading chunk failed. Error %s: %s' % (code,message)

        if not chunkOK:
            # We did three tries and the chunk still failed.
            print 'Uploading chunk failed after three attempts. File: %s Id: %s Offset: %s Attempt: %s' % (path,fileid,offset,attempts)
            exit(1)

        # Store the fileID so we can reuse it
        fileid = api.parseID(content,'file','id')

        # Get the offset the server has already (to support resuming uploads)
        offset = api.parseID(content,'file','offset')

        # Make sure we don't upload past the end of the file!
        if offset + chunksize > size:
            chunksize = size - offset

    # All chunks uploaded
    # Close the file handle
    fh.close()

    return fileid

def layoutRegionMediaDelete(api,layoutid,regionid,mediaid,lkid):
    params = [('mediaId',mediaid),
              ('regionId',regionid),
              ('layoutId',int(layoutid)),
              ('lkId',int(lkid))
             ]
    
    response, status, code, message, content = api.callMethod('LayoutRegionMediaDelete', params)

def libraryMediaDelete(api,mediaid):
    params = [('mediaId',mediaid)]

    response, status, code, message, content = api.callMethod('LibraryMediaDelete', params)

#############################################
# END LIBRARY FUNCTIONS
#############################################
  • So first up we upload the new file to the CMS. Note here that by upload we mean copy it to the server ready to be added to the library. We add it to the library in a second stage later on. Add the following to upload:
# Upload our file to the CMS so it's ready to add
# Note this won't make it appear in the library
# We do that further down the script
uploadId = uploadFile(api,fileToUpload)
  • Next we need to find the ID of the old video file, remove it from the Region Timeline, and then delete it from the library. Add the following:
# Find out what the mediaID of the old video is, and remove it from the layout
oldMediaId = None

params = [('layoutid', layoutId),
          ('regionid', regionId)]

# Call the RegionList Method
response, status, code, message, content = api.callMethod('LayoutRegionTimelineList', params)

# Parse the response XML
doc = minidom.parseString(content)

# List the Regions
nodes = doc.getElementsByTagName('media')

for node in nodes:
    if str(node.attributes['mediatype'].value) == 'video':
        oldMediaId = str(node.attributes['mediaid'].value)
        lkid = str(node.attributes['lkid'].value)

if not oldMediaId is None:
    # We found an old video in the region
    # Lets delete it
    print "Removing old media with ID %s" % oldMediaId
    layoutRegionMediaDelete(api, layoutId, regionId, oldMediaId, lkid)
    libraryMediaDelete(api, oldMediaId)
  • Now the old video is gone from the layout timeline, and from the CMS Library, we can add our new video to the library and add it to the region. Add the following:
# Now the file is uploaded, and the old file is deleted,
# we need to add the newly uploaded file to the CMS Library
params = [('fileId', uploadId),
          ('type', 'video'),
          ('name', mediaName),
          ('duration', '0'),
          ('permissionId','1'),
          ('fileName',os.path.basename(fileToUpload))
         ]

response, status, code, message, content = api.callMethod('LibraryMediaAdd', params)

# Get the ID of the file we just added
mediaId = api.parseID(content,'media')

# Now mediaID contains the library media ID of our freshly uploaded video
print "Adding new media with ID %s" % mediaId

# Now add our new media item to the region
params = [('layoutId',layoutId),
          ('regionId',regionId),
          ('mediaList',mediaId),
         ]

response, status, code, message, content = api.callMethod('LayoutRegionLibraryAdd', params)
  • Finally check that the video was added correctly by listing the media items in the region. Add the following:
# Finally Check the layout now contains the new video
# by listing the contents of the region and checking for our new video
params = [('layoutId', layoutId),
          ('regionId', regionId)]

# Call the RegionList Method
response, status, code, message, content = api.callMethod('LayoutRegionTimelineList', params)

# Parse the response XML
doc = minidom.parseString(content)

# List the Regions
nodes = doc.getElementsByTagName('media')

for node in nodes:
    if str(node.attributes['mediatype'].value) == 'video':
        if str(node.attributes['mediaid'].value) == str(mediaId):
            print "Video Uploaded and Layout Modified Successfully"
            exit(0)

print "Error Uploading Video"
  • Now save and run your file. You should see output as follows:
alex@alex-laptop:~/scratch/xibo-release/ReplaceVideo$ python ReplaceVideo.py 
Removing old media with ID 15
Adding new media with ID 16
Video Uploaded and Layout Modified Successfully

You can download the completed ReplaceVideo.py file here.

Support for Ubuntu 12.04

Perhaps one of the most frequently asked questions we get over on Launchpad Answers is “Does the Python client run on Ubuntu 12.04 yet”.

Until the release of 1.6.0-rc1, the answer has always been no. However thanks to the work of several contributors to the Berkelium project, Eng Chong Meng, and to Jianjian, we now have a version of the client that runs on Ubuntu 12.04 and hopefully has the pesky text rendering bugs from the 1.4 series client resolved.

If you have an Ubuntu 12.04 32 bit PC you’d like to install the client on, please see the getting started guide in the manual.

Since Ubuntu 10.04 is no longer officially supported by Canonical, we’ve dropped support for it from the Python client installer as of version 1.6.0-rc1. However, there’s no reason that if you’re otherwise happy with 1.4.2 on Ubuntu 10.04 that you can’t upgrade to 1.6.0 by the following method:

  1. Open a terminal
  2. cd /opt/xibo/pyclient
  3. bzr pull lp:xibo/1.6

That will update your installed client to version 1.6.x (whatever the latest 1.6 release is at the point you run the command). It will replace your libbrowsernode.so file in the client directory with a version that’s not compatible with Ubuntu 10.04.

To resolve that, issue the following command in the terminal:

  1. cd /opt/xibo/pyclient/client/python
  2. cp /usr/local/lib/python2.6/dist-packages/libavg/libbrowsernode.so.0.0.0 libbrowsernode.so

You should then be able to run the client as normal.

Going forward, the Berkelium project has now been abandoned in favour of Chromium Embedded Framework. If you have the skills to port libbrowsernode to use Chromium Embedded Framework instead of Berkelium then please get in touch as we feel that’s where this client’s future lies.

Become a Support Hero!

Using Xibo? Know your way around it well? Could you spare a few minutes each day to help others in the community and give something back?

As Xibo has become more popular, the number of support requests in Launchpad Answers has rocketed, to the point that the two main developers (Dan and Alex) just can’t answer every request ourselves. It takes time to replicate each person’s setup to see if the problem is general, or something local to that installation. That’s where we need the most help.

To those who do already help out with Answers, our sincere thanks. By freeing our time from answering support requests, you’re actively assisting new code for Xibo to be written and new features to be incorporated faster.

If you think you could lend a helping hand to those just starting out with Xibo, or to those who are exploring some of the more complex feaures, you can help in the following ways:

  • Sign up to be a Xibo Answers Contact. Sign in with your Launchpad ID, or create an account if you don’t have one. With this option you’ll receive email when there is activity in Launchpad Answers for Xibo.
  • Or, if you don’t want the email, drop by Launchpad Answers from time to time. Questions shown as “Open” or “New” are awaiting a response so dive in if you think you can help.

We know some of the documentation for Xibo isn’t great, and that generates a fair amount of the traffic we’re seeing in Launchpad Answers at present. We were very lucky to be contributed a compiled “manual” for Xibo, which brings together all the existing Xibo documentation in to one place. We’re currently working on getting that updated, integrated and release ready for 1.6 series of Xibo and beyond.

If you have any queries or need help getting registered as an Answers contact, feel free to contact me at alex@xibo.org.uk

Updated 1.4.2 and 1.2.3 releases now available

There was a small issue found with the installer in versions 1.4.2 and 1.2.3. It meant that people installing those versions may find their server key has been set incorrectly.

If you’ve already installed 1.4.2 or 1.2.3, you’re strongly recommended to upgrade to the versions we’ve just uploaded (with a .2 suffix on the download file) which contain a fix for this, as well as a change to some internal validations that the system does.

Once upgraded, you’re strongly recommended to check your server key appears as you expect in the server settings.

Apologies for any inconvenience caused.