Video broadcasting and recording with Cisco. Annex 1. HLS

HLS (HTTP Live Streaming) is a live video broadcasting protocol by Apple. It’s current standartization status is IETF draft. It splits incoming stream coded with MPEG (H.264 и AAC) into little chunks (*.ts files) and forms a playlist (*.m3u8) which is served to users. When a users queries for a stream his client (browser) start a simple file download process chunk by chunk. Visit https://developer.apple.com/streaming/ for more information.
This protocol is supported by Safari for MAC and IOS, stock browser and Chrome for Android. Visit http://www.jwplayer.com/html5/hls/ for more info. On a desktop with Windows or Linux you can use VLC player to open m3u8.

Advantages:
– pure HTML5: <video src=’http://<server name>/playlist.m3u8′ width=’640′ height=’360′ controls=’controls’></video> – and that’s it.
– there is no need for a browser plugins, especially so vunerable as flash
– supports AES and adaptive bitrate.

In our case we need another configuration for Wowza and nginx retranslators.

1) For Wowza create a Live origin app (HTTP) and edit /usr/local/WowzaMediaServer/conf/live/Application.xml

<HTTPStreamer>
<Properties>
    <Property> 
        <Name>httpOriginMode</Name> 
        <Value>on</Value> 
    </Property> <!-- Apple HLS: cache control --> 
    <Property> 
        <Name>cupertinoCacheControlPlaylist</Name>
        <Value>max-age=1</Value>
    </Property>
    <Property> 
        <Name>cupertinoCacheControlMediaChunk</Name> 
        <Value>max-age=3600</Value> 
    </Property> <!-- Smooth Streaming: cache control --> 
    <Property> 
        <Name>smoothCacheControlPlaylist</Name> 
        <Value>max-age=1</Value> 
    </Property> 
    <Property> 
        <Name>smoothCacheControlMediaChunk</Name> 
        <Value>max-age=3600</Value> 
    </Property> 
    <Property> 
        <Name>smoothCacheControlDataChunk</Name> 
        <Value>max-age=3600</Value>
    </Property> <!-- Flash HDS: cache control --> 
    <Property> 
        <Name>sanjoseCacheControlPlaylist</Name> 
        <Value>max-age=1</Value> 
    </Property> 
    <Property> 
        <Name>sanjoseCacheControlMediaChunk</Name> 
        <Value>max-age=3600</Value> 
    </Property> 
    <Property> 
        <Name>cupertinoOnChunkStartResetCounter</Name> 
        <Value>true</Value> 
        <Type>Boolean</Type> 
    </Property>
</Properties>
</HTTPStreamer>

2) And convert nginx to a simple proxy server

http {
include mime.types;
default_type application/octet-stream;
sendfile on;
tcp_nopush on;
proxy_cache_path /tmp/hls_cache levels=1:2 keys_zone=pcache:1024m max_size=10048m inactive=10d use_temp_path=off;
client_header_timeout 10m;
client_body_timeout 10m;
send_timeout 10m;
connection_pool_size 256;
client_header_buffer_size 1k;
large_client_header_buffers 4 2k;
request_pool_size 4k;
keepalive_timeout  65;
        server {
                listen 1936;
                server_name <server name>:1936;
                location / {
                        types {
                            application/vnd.apple.mpegurl m3u8;
                        }
                        proxy_cache pcache;                        
                        proxy_pass http://<wowza ip>:1935;
                }
                }
}

3) Now you can use your favorite player (flowplayer would work too). And serve users your broadcast.

Video broadcasting and recording with Cisco part 1 update. CUCM+TCS 6.2

So, Cisco updated Telepresence content server to version 6.2 and provided no release notes. And new documentation leaves much to be desired too (just look at the screenshots at http://www.cisco.com/c/en/us/td/docs/telepresence/tcs/6_2/administration/guide/tcs_6_2/cucm.html – can you guess what’s on them?). I’ve managed to upgrade my TCSs and made them work. 1) There is no upgrade tool presented on Cisco download portal. So I just got tcs_6.2_vm_package.zip, extracted it and ran S6_2_VM.exe on the machine with TCS 6.1. 2) After the install is completed, navigate to Content Server Web interface Management -> Configuration -> Site settings and make SIP setting section looks like that 2015-06-11_1337 (click to enlarge)

Note: 192.168.20.1 is an IP address of Content Server and 192.168.0.1 – CUCM 3) Now it’s time to tune our trunks on CUCM. Navigate to Unified CM Administration -> System -> Security -> SIP Trunk Security Profile and create a new one with these settings:

2015-06-11_1342

4) Now navigate to Unified CM Administration -> Device -> Device Settings -> SIP Profile and create a new one named SIP Trunk profile for TCS. Copying a standart one would do, but don’t forget to tweak it like that:

2015-06-11_1347

5) Final step: go to SIP trunk you’ve created here https://profcollab.wordpress.com/2015/06/04/video-broadcasting-and-recording-with-cisco-part-1-cucmtcs/ and change its SIP Trunk Security Profile and  SIP Profile to the ones you’ve just created. Cisco also recommends to set Rerouting Calling Search Space and Out-Of-Dialog Refer Calling Search Space to something reasonable.

Video broadcasting and recording with Cisco part 5. flowplayer

Now let’s install web-server which will serve a web-page with flowplayer which will display our live broadcast. We will instruct our users to open link like player.example.com/?stream=1002 in order to start browsing the broadcast.

1) Let’s install Apache and WSGI

# sudo apt-get update
# sudo apt-get upgrade
# sudo apt-get install apache2
# sudo apt-get install libapache2-mod-wsgi

2) Install Flask framework

# wget https://bootstrap.pypa.io/get-pip.py
# python get-pip.py
# python -m pip install flask

3) Inside /var/www/html create wsgi and put player.wsgi inside

#player.wsgi
 import sys
 sys.path.append('/var/www/html/wsgi')
 from player import app as application

4) Inside wsgi folder create player.py

# -*- coding: utf-8 -*-
from flask import Flask, jsonify, render_template, request, redirect, url_for
app = Flask(__name__)
import httplib
import urllib2
import xml.etree.ElementTree as ET


def get_nclients(host):
    try:
        passman = urllib2.HTTPPasswordMgrWithDefaultRealm()
        passman.add_password(None, 'http://'+host+':8080', '', '')
        authhandler = urllib2.HTTPDigestAuthHandler(passman)
        opener = urllib2.build_opener(authhandler)
        res = opener.open('http://'+host+':8080/stat',timeout=2)
        response = res.read()
        res.close()
        summ=0
        tree = ET.fromstring(response)        
        serv=tree.find('server')
        #print serv[0][0].text
        #print serv[1][0].text
        for app in serv:
            for stream in app[1]:
                for clients in stream.findall('nclients'):
                    summ=summ+int(clients.text)
        return summ    
    except:
        return 999

def streams(host):
    result=[]
    try:
        url = 'http://'+host+':8086/connectioncounts'
        mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
        mgr.add_password(None,url,'***','***')
        opener = urllib2.build_opener(urllib2.HTTPBasicAuthHandler(mgr), urllib2.HTTPDigestAuthHandler(mgr))
        urllib2.install_opener(opener)
        f = urllib2.urlopen(url,timeout=2)
        tree = ET.fromstring(f.read())
        vhost = tree.find('VHost')
        for stream in vhost.iter('Stream'):
            result.append(stream.find('Name').text)
    except:
        result=[]
    return result

@app.route("/")
def hello():
    stream=request.args.get('stream')
    rtlrs=['192.168.10.1','192.168.11.1','192.168.12.1']
    rtlr_found=False
    while not rtlr_found:
               temp=[]
               for i in rtlrs:           
                    nclients=get_nclients(i)
                    if nclients

Logic behind this script is following.

a) When users visit our url first of all we get stream number they are looking for with stream=request.args.get(‘stream’) inside hello function

b) Assume we have a number of nginx retranslators [‘192.168.10.1′,’192.168.11.1′,’192.168.12.1’]. Let’s query them for a number of connected users and choose least loaded one. get_nclients function will help us with it. If there are more that 700 users connected to a certain retranslator we won’t use it.

c) We also need to be sure that stream is present as Wowza server, so let’s query it for all it streams with streams function. If there is no stream let’s inform users about it. Do not forget to change Wowza admin passwords inside this function.

d) Finally generate index.html and pass url for a stream to it.

5) Index.html will look like that (don’t forget to delete hashtags in the beginning of every line):

#<!DOCCTYPE html>
#<html><head>
#<meta http-equiv="content-type" content="text/html; charset=UTF-8">
#<meta charset="utf-8">
#<head>
# <link rel="stylesheet" href="//releases.flowplayer.org/6.0.1/skin/minimalist.css">
# //code.jquery.com/jquery-1.11.2.min.js
# //releases.flowplayer.org/6.0.1/flowplayer.min.js
#</head>
#<title>Player</title>
#</head>
#<body>
#
#toggle video #
# # # #
#</div> #</body> #</html>

6) And errors.html like that (don’t forget to delete hashtags in the beginning of every line):

#<!DOCCTYPE html>
#<html><head>
#<meta http-equiv="content-type" content="text/html; charset=UTF-8">
#<meta charset="utf-8">
#<title>Player</title>
#</head>
#<body>
#<span id="debug">{{error}} </span>
#</body>
#</html>

7) Final step is to edit virtual hosts in Apache config at /etc/apache2/sites-enabled/000-default.conf (don’t forget to delete hashtags in the beginning of every line):

     #<VirtualHost *>
     #ServerName 
     #DocumentRoot /var/www/html
     #WSGIDaemonProcess player
     #WSGIScriptAlias / /var/www/html/wsgi/player.wsgi
     #<Directory /var/www/wsgi>
         WSGIProcessGroup player
         WSGIApplicationGroup %{GLOBAL}
         Order deny,allow
          Allow from all
     #</Directory>

8) Restart Apache with service apache2 restart and you are ready to go. In case of ploblems with flowplayer consult http://demos.flowplayer.org/

Video broadcasting and recording with Cisco part 4. Wowza+Nginx

During these series we have installed Cisco Telepresence Content Server and Wowza Media Streaming Engine. TCS works as SIP gateway and converts SIP signalling with RTP payload to RTSP signalling with RTP for live streams. Wowza accepts RTSP and converts it to RTMP which can be used with Flash players and HLS for HTML5 (we will discuss HLS later).
Imagine that you have to provide live streaming for more than 1000 users. In that case Wowza have a solution – Wowza Origin and Wowza Edge. The idea behind it that you have one generating Wowza server and a number of retranslators (edges), which are situated closer to users. That’s great but you have to buy license for all those edges and it summs up to a big money. Maybe it’s worth investing when you have big video broadcasting events every week, but what if your weekly translations cover 700 users and such big events are held once a year. In that case there is open-source solution for retranslators: nginx web server with nginx-rtmp plugin (https://github.com/arut/nginx-rtmp-module).

1) Assume we have fresh installation of Ubuntu 14.04. Let’s install all supplementary packages.

# apt-get update
# apt-get upgrade
# apt-get install build-essential libpcre3 libpcre3-dev libpcrecpp0 libssl-dev zlib1g-dev git checkinstall

2) Create separate user for nginx

# useradd --no-create-home nginx

3) Download nginx and rtmp plugin sources

# mkdir /usr/build 
# cd /usr/build 
# git clone git://github.com/arut/nginx-rtmp-module.git 
# wget http://nginx.org/download/nginx-1.7.11.tar.gz

4) Unpack, configure, compile and install package. Again I’m using checkinstall to get deb packet and the ability to delete installed package with dpkg in future

# tar zxfv nginx-1.7.11.tar.gz
# cd nginx-1.7.11
# ./configure --prefix=/usr/nginx --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --pid-path=/var/run/nginx.pid --lock-path=/var/lock/nginx.lock --with-http_ssl_module --user=nginx --group=nginx --with-http_stub_status_module --with-http_gzip_static_module --add-module=/usr/build/nginx-rtmp-module
# make
# checkinstall --pkgname nginx-rtmp

5) Create a script to launch nginx

# cd /etc/init.d
# vi nginx

Copy following to vi

# Description: starts nginx using start-stop-daemon
### END INIT INFO
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/nginx/sbin:/usr/nginx/bin
DAEMON=/usr/nginx/sbin/nginx
NAME=nginx
DESC=nginx
test -x $DAEMON || exit 0
# Include nginx defaults if available
if [ -f /etc/default/nginx ] ; then
. /etc/default/nginx
fi
set -e
. /lib/lsb/init-functions
case "$1" in
start)
echo -n "Starting $DESC: "
start-stop-daemon --start --quiet --pidfile /var/run/$NAME.pid \
--exec $DAEMON -- $DAEMON_OPTS || true
echo "$NAME."
;;
stop)
echo -n "Stopping $DESC: "
start-stop-daemon --stop --quiet --pidfile /var/run/$NAME.pid \
--exec $DAEMON || true
echo "$NAME."
;;
restart|force-reload)
echo -n "Restarting $DESC: "
start-stop-daemon --stop --quiet --pidfile \
/var/run/$NAME.pid --exec $DAEMON || true
sleep 1
start-stop-daemon --start --quiet --pidfile \
/var/run/$NAME.pid --exec $DAEMON -- $DAEMON_OPTS || true
echo "$NAME."
;;
reload)
echo -n "Reloading $DESC configuration: "
start-stop-daemon --stop --signal HUP --quiet --pidfile /var/run/$NAME.pid \
--exec $DAEMON || true
echo "$NAME."
;;
status)
status_of_proc -p /var/run/$NAME.pid "$DAEMON" nginx && exit 0 || exit $?
;;
*)
N=/etc/init.d/$NAME
echo "Usage: $N {start|stop|restart|reload|force-reload|status}" >&2
exit 1
;;
esac
exit 0

5) Make it executable and add to autolaunch. Now you can start, stop and restart it with

# /etc/init.d/nginx start|stop|restart command
# chmod +x /etc/init.d/nginx
# update-rc.d -f nginx defaults

6) Now post following to /etc/nginx/nginx.conf and restart nginx

worker_processes  1;
error_log /var/log/nginx/error.log  debug;
events {
worker_connections 1024;
}
rtmp {
    server {
        listen 1935;
        application live-edge {
                live on;
                pull rtmp://<wowza IP adress>:1935 app=live;
       }
    }
}

http {
        server {
                listen 8080;
                location /stat {
                        rtmp_stat all;
                        rtmp_stat_stylesheet stat.xsl;
                }
                location /stat.xsl {
                        root /usr/build/nginx-rtmp-module/;
                }
               location /control {
                        rtmp_control all;
                }
        }
}

RTMP section of the config creates RTMP server that serves live-egde RTMP application. Whenever user opens browser with flash player, the player makes a connection to this nginx server and provides app name (live-edge) and stream number (1002). When nginx gets this request it connects to Wowza and query for app=live and same stream name. When second user comes it servres him chached data without creating additional connection to Wowza.  You can as many such retranslators as you want.

Additionally you can query nginx for connected users by consulting http://<server IP>:8080/stat.

Video broadcasting and recording with Cisco part 3. TCS API

In part 1 of these series we created two aliases 1001 and 1002 and the first one was used for recording. Cisco TCS has it’s own interface for downloading records and provides opportunity to add description to records (recording aliases to be exact) in order to help you identify then later. TCS is build on Windows so you can add it to your domain controller and authenticate users with it. I don’t have one and don’t like the idea of users even browsing TCS interface so here is a simple Python  class with three methods: first for listing all calls on TCS, second is for adding description before recording and last one for obtaining a list of download URLs for recorded streams.

import urllib2 
from suds.client import Client 
from suds.transport.https import HttpAuthenticated 
from suds import WebFault
from datetime import datetime
import xml.etree.ElementTree as ET
TCS_API_HOSTNAME='192.168.0.10'
TCS_API_URL='/tcs'
TCS_API_USERNAME='admin'
TCS_API_PSW='admin' # do not forget to change API password in TCS management interface
TCS_WSDL='http://'+TCS_API_HOSTNAME+TCS_API_URL+'/Helium.wsdl'
TCS_API_PROXY='http://'+TCS_API_HOSTNAME+TCS_API_URL+'/SoapServer.php'
TCS_API_ROOT='http://'+TCS_API_HOSTNAME+TCS_API_URL
class tcs_api_handler(object):     
    def get_current_cf_list(self):    # getting layout of current calls as aliases with their ids        
        passman = urllib2.HTTPPasswordMgrWithDefaultRealm()        
        passman.add_password(None, TCS_API_ROOT, TCS_API_USERNAME, TCS_API_PSW)        
        authhandler = urllib2.HTTPDigestAuthHandler(passman)        
        opener = urllib2.build_opener(authhandler)        
        res = opener.open(TCS_API_ROOT+'/status.xml')        
        response = res.read()        
        res.close()        
        tree = ET.fromstring(response)        
        cf_list={}        
        for call in tree.iter('{http://www.tandberg.no/XML/CUIL/1.0}Call'):            
            cf_id=call.find('{http://www.tandberg.no/XML/CUIL/1.0}ConferenceId').text            
            alias_full=call.find('{http://www.tandberg.no/XML/CUIL/1.0}SIPURI').text            
            cf_list[alias_full.split("@")[0]]=cf_id        
        return cf_list                           
    def prepair_alias(self, alias,description):        
        t = HttpAuthenticated(username=TCS_API_USERNAME, password=TCS_API_PSW)        
        t.handler=urllib2.HTTPDigestAuthHandler(t.pm)        
        t.urlopener = urllib2.build_opener(t.handler)        
        client = Client(TCS_WSDL, location=TCS_API_PROXY, transport=t)                        
        res=client.service.GetRecordingAlias(Alias=alias)        
        res.RecordingDescription=description        
        try:            
            res1=client.service.ModifyRecordingAlias(Alias=alias, Data=res)            
            return res1        
        except WebFault as err:            
            return err            
    def get_donwloadlink_all(self):        
        t = HttpAuthenticated(username=TCS_API_USERNAME, password=TCS_API_PSW)        
        t.handler=urllib2.HTTPDigestAuthHandler(t.pm)        
        t.urlopener = urllib2.build_opener(t.handler)        
        client = Client(TCS_WSDL, location=TCS_API_PROXY, transport=t)                        
        userparams = {'SearchExpression':'','ResultRange':'','DateTime':'','UpdateTime':'','Owner':'','Category':'','Sort':''}        
        res=client.service.GetConferences(**userparams)        
        res1={}        
        for cf in res[0]:            
            time=datetime.fromtimestamp(int(cf['DateTime']))            
            if cf['HasDownloadableMovie']==True:                
                res1[str(cf['ConferenceID'])]= [unicode(cf['Title']).split('$')[0],str(cf['Description']),str(datetime.fromtimestamp(int(cf['DateTime']))), str(cf['DownloadableMovies'][0][0]['URL'])]             
        return res1

So every time recording alias is added to the conference I issue prepair_alias(‘1001′,’Some descriptions’) and when the recording is done and TCS finished transcoding it I can use  get_donwloadlink_all to list all downloadable movies along with their description, timestamp and alias used for recording.

Video broadcasting and recording with Cisco part 2. TCS+Wowza

Last time we created two aliases on Cisco Telepresence Content Server – 1001 for recording and 1002 for live streaming. Up to this step we are able to call these aliases (or add them to conference) to record or broadcast our call or conference.

But TCS can generate RTSP stream and it’s not an obvious task how to integrate this stream into user’s browsers. Again – usually you have 1G link on TCS server and it will allow approximately 500 connections to HD-quality streams.

We are going to use Wowza Streaming Engine to converts RTSP streams to RTMP/HLS/HDS streams that can be served to various flash/html5 players. At the same time we will use Wowza to down-rate streams to lower qualities – not all users have good Internet connection to allow HD streams.

1) Register and download Wowza Streaming Engine from http://www.wowza.com/.
2) Save license key and  WowzaStreamingEngine-4.1.1.deb.bin installation
3) I’m using Ubunty Trusty at the moment. Let’s install Java

# apt-get update
# apt-get upgrade
# apt-get install default-jre
# apt-get install default-jdk

4) Move to the folder with WowzaStreamingEngine-4.1.1.deb.bin and make it executable

# chmod +x WowzaStreamingEngine-4.1.1.deb.bin
#  ./WowzaStreamingEngine-4.1.1.deb.bin

5) During the installation you are prompted to enter license key, create admin credentials and will ask to add itself to autolaunch

6) Now Wowza is ready. Installation path is  /usr/local/WowzaStreamingEngine/.

To start service sudo service WowzaStreamingEngine start
To stop service sudo service WowzaStreamingEngine stop

7) Download WowzaStreamingEngine-Update-4.1.2.zip from  http://www.wowza.com/ and move it to /usr/local/WowzaStreamingEngine/updates/

8) Unpack update file

# apt-get install unzip
# mkdir /usr/local/WowzaStreamingEngine/updates/WowzaStreamingEngine-Update-4.1.2/
# unzip /usr/local/WowzaStreamingEngine/updates/WowzaStreamingEngine-Update-4.1.2.zip –d /usr/local/WowzaStreamingEngine/updates/WowzaStreamingEngine-Update-4.1.2/

9) Stop all Wowza services

# service WowzaStreamingEngine stop
# service WowzaStreamingEngineManager stop

10) Move to folder with update script and launch it

# /usr/local/WowzaStreamingEngine/updates/WowzaStreamingEngine-Update-4.1.2/linux
# chmod +x *.sh
# ./update.sh

11) After update go to  http://<wowzaserver_ip&gt;:8080/  and login with credentials from step 5.

12) Navigate to Applications -> Add Application -> Live (single server or origin) and enter  _defapp_

13) Navigate to Live Applications -> Live. In list below select Incoming Security and make it look like

14) Now navigate to Server -> Publishers -> Add publisher and add new publisher name and password (they will be used lated when configuring Cisco TCS)

15) Edit /usr/local/WowzaStreamingEngine/conf/live/Application.xml

16) Search for Properties and RTP sections – they should look like:

<Properties>
  <Property>
    <Name>sortPackets</Name>
    <Value>true</Value>
    <Type>Boolean</Type>
  </Property>
  <Property>
    <Name>sortBufferSize</Name>
    <Value>500</Value>
    <Type>Integer</Type>
  </Property>
 </Properties>

 <RTP>
  <Authentication>
    <PublishMethod>digest</PublishMethod>
    <PlayMethod>digest</PlayMethod>
  </Authentication>

17) Move to Cisco TCS administration web-interface. Navigate to Management -> Recording Setup -> Media server configurations -> +Add Wowza Media Server for Flash configuration

18) Set the parameters

Name: Wowza 1002
Server address:wowzaserver_ip>
Support live unicast streaming: Yes
User name/Password/Password confirm: see step 14
Use default live URL: Yes
Application directory: live
Static stream name (optional): 1002

19) Hit save. If everything is ok you’ll see:

20) Navigate to Management -> Recording Setup -> Templates -> Live and set

Media server configuration to Wowza 1002

21) Call 1002 from you video codec, navigate to Wowza administration interface Applications -> Live -> Incoming streams, choose 1002, hit Test players in upper right corner, choose appropriate player and you’ll be able to monitor your broadcasting stream.

Video broadcasting and recording with Cisco part 1. CUCM+TCS

I’m lucky enough to get my hands on Cisco Telepresence Content Server and in this series I”m going to show how to setup video broadcasting and streaming with Cisco UCM, TCS, Wowza Media Sreaming Engine, nginx and flowplayer. In the end we will able to stream every conference or call from or to Cisco codec controlled by CUCM to user’s browser.

Let’s start with Cisco Unified CM. All we need here is to set up SIP trunk and route pattern which will point to that trunk.

1) Navigate to Cisco Unified CM Administration -> Device -> Trunk -> Add new and create SIP trunk with following parameters

Product:  SIP Trunk
Device Protocol:  SIP
Trunk Service Type: None(Default)
Device Name: cisco_tcs
Device Pool: (depends on your installation)
Call Classification: On-net
Location:  (depends on your installation)
Calling Search Space:  (depends on your installation)
SIP Information Destianation: IP address of TCS
SIP Trunk Security Profile: Non secure SIP trunk profile for Conductor (my CUCM has it preinstalled)
SIP Profile:  SIP trunk for Conductor (you can check http://www.cisco.com/c/dam/en/us/td/docs/telepresence/infrastructure/conductor/config_guide/xc3-0_docs/TelePresence-Conductor-Certificate-Deployment-Guide-XC3-0.pdf for more details on these profiles)

All other parameters marked with * (that is they are mandatory) are good with defaults.

2) Navigate to Cisco Unified CM Administration -> Call Routing -> Route/Hunt -> Route pattern -> Add new and create new pattern that can be called from video codecs

Route Pattern: 100[1-2] (for the sake of simplicity 1001 is going to be used for recording and 1002 for streaming)
Gateway/Route List: cisco_tcs

That’s it to CUCM – now codecs can dial 1001 and 1002 provided Partition and CSS setups are allright.

3) Let’s go to Cisco TCS web page ( <ip address of the server>/tcs) and navigate to Management -> Recording setup -> Templates and add a couple

1st for recording with following settings

2nd for live streaming

Note that there is a field for Media server configuration – we will get to it in part 2.

4) Navigate to Management -> Recording setup -> Recording Alias and add 2 aliases

1st – 1001 for recording (using On demand template)

2nd – 1002 for streaming

That’s it. Now we are able to call 1001 and this call will be recorded. You can download it later using TCS web interface and I will show how to get download URL with TCS API.

Monitoring device registration with CUCM Serviceability AXL API

Recently I started to receive more and more complaints from my users that my Cisco video codecs are not working. The problem was that these same users had unplugged ethernet cords from the codecs so they lost registration with CUCM. So I decided to create some tool to monitor registration status of video endpoints and possibly 7937 conference phones (they’ve got unplugged too).

I decided that CUCM Serviceability API (https://developer.cisco.com/site/collaboration/management/uc-manager-serviceability/overview/) would do.
1) Let’s install suds module (and we will need python 2.7.9 with ssl module too)

cd ~/Downloads
sudo wget https://bootstrap.pypa.io/get-pip.py
sudo python get-pip.py
sudo python -m pip install suds
cd ~/
vi srv_xml_api.py

2) Inside srv_xml_api.py let’s import only necessary parts:

import sslimport urllib2from suds.xsd.doctor 
import Importfrom suds.xsd.doctor 
import ImportDoctorfrom suds.client 
import Clientfrom suds.transport.https 
import HttpAuthenticatedfrom datetime 
import datetime, timedelta

3) Let’s also define global variable MODELS – a dictionary with device’s model id as it appears in CUCM database and its description:

MODELS={'626':'Cisco TelePresence SX20',        
        '682':  'Cisco TelePresence SX10',
        '608':  'Cisco TelePresence Codec C40'}

4) We will also need path to CUCM Serviceability API, and application user credentials to access it.

SRV_CMSERVER='192.168.0.1' #IP of CUCM
SRV_axl_port='8443'
SRV_user='srv_api_user'
SRV_passwd='****'
SRV_wsdl='https://'+SRV_CMSERVER+':'+SRV_axl_port+'/realtimeservice/services/RisPort?wsdl'
SRV_location='https://'+SRV_CMSERVER+':'+SRV_axl_port+'/realtimeservice/services/RisPort'

Note that  srv_api_user should be a part of AXLUsers, Standard CCM Admin Users and Standart CCM Super Users groups.

4) Here comes the one and only class cmdevices. It will have cur_layout and prev_layout vars – lists of dictionaries where we will store device name (as it appears in CUCM), directory number, description, IP, registration status, and model. When initializing in stance of the class I’m going to run update_current_states() method to populate cur_layout (we’ll come to this method later), and add Stat_change_at and Stat_change_reason keys to layouts to indicate when registration status has changed and what what the reason for that.

class cmdevices(object):
    cur_layout=[]  
    prev_layout=[]  
    def __init__(self):      
         self.update_current_states()      
         self.prev_layout=self.cur_layout      
         for i in range(len(self.prev_layout)):          
             self.prev_layout[i]['Stat_change_at']=datetime.now()          
             self.prev_layout[i]['Stat_change_reason']= '1strun'

5) Main method of the class would be update_current_states. It’s going to communicate with CUCM, and query it for device states.

def update_current_states(self):
    layout=[]     
    layout_reduce=[]     
    for model in MODELS:         
        t = HttpAuthenticated(username=SRV_user, password=SRV_passwd)         
        t.handler=urllib2.HTTPBasicAuthHandler(t.pm)         
        ssl_def_context = ssl.create_default_context()         
        ssl_def_context.check_hostname = False         
        ssl_def_context.verify_mode = ssl.CERT_NONE         
        t1=urllib2.HTTPSHandler(context=ssl_def_context)         
        t.urlopener = urllib2.build_opener(t.handler,t1)         
        tns = 'http://schemas.cisco.com/ast/soap/'         
        imp=Import('http://schemas.xmlsoap.org/soap/encoding/', 'http://schemas.xmlsoap.org/soap/encoding/')         
        imp.filter.add(tns)
        client=Client(SRV_wsdl,location=SRV_location,plugins=[ImportDoctor(imp)],transport=t)         
        result = client.service.SelectCmDevice('',{'SelectBy':'Name', 'Class':'Phone', 'Model':model})         
        for node in result['SelectCmDeviceResult']['CmNodes']:             
            for device in node['CmDevices'] 
                layout.append({ 'Name':str(device['Name']),                                 
                                 'DN':str(device['DirNumber'])[:4],
                                 'Description':str(device['Description']),
                                 'IP': str(device['IpAddress']),
                                 'Status':str(device['Status']),
                                 'Model':str(MODELS[device['Model']])})     
       
    return True

First part is obvious – we are connecting to CUCM using default ssl context with Basic Auth and mend our schema with Doctor from suds. But I noticed that of one of the devices lost registration with it’s first CUCM from CM_Group and register with the second Serviceability API will list this device as unregistered with 1st node and registered with the second. Let’s call this status split and assume that in that case device is registered. So you have to parse returne layout to sort this out.

6) Last method of the class you have to implement will fetch for current states from CUCM and analyze changes comparing cur_layout and prev_layout

7) That’s it – in the main part of the script you should create an instance of the class and call update_changes periodically (I personally use apscheduler python module for that) display information you are interested in from cur_layout of the instance. You can go beyond with it as I did and use Flask framework to launch web-server and show device registration status on a web page with automatic refresh.

SSH to Cisco video codecs using Python

Recently I decided to re-register all my Cisco SX20 codecs, which were controlled by Cisco VCS to Cisco Unified CM. Apart from other parameters you should provide to CUCM you need to know device’s MAC addresses. I believe that there are numerous ways to do that, but as a Python fan I’m going to show you how to do it with a simple script. By the way you can use it to connect to Cisco routers and switches (and it differs with the way you use Python to log in to Linux machine)

1) Let’s get pip, install pamiko module and create script

cd ~/Downloads
sudo wget https://bootstrap.pypa.io/get-pip.py
sudo python get-pip.py
sudo python -m pip install pamiko
cd ~/
vi python-ssher.py

2) Here comes the script. Let’s import libraries and define our hosts

import paramiko
import time
hosts=['192.168.0.1','192.168.0.2','192.168.0.3']

3) Now – the main function.

def get_mac(host):
    client = paramiko.SSHClient()
    client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    client.connect(hostname=host, username='***', password='***', port=22,  look_for_keys=False, allow_agent=False)
    client_shell = client.invoke_shell()
    client_shell.recv(1024)
    time.sleep(2)
    client_shell.send("xstatus Network Ethernet\n")
    time.sleep(2)
    output= client_shell.recv(1024)
    client.close()
    time.sleep(2)
    return output[output.find('"')+1:output.find('"')+18]

Tips:
– change username and password in client.connect callback.
– I’ve also added time.sleep(2) between executing commands – you need to give our client some time to read from output.
– after  invoke_shell() I’ve added recv(1024) to skip MOTD.
– after sending xstatus Network Ethernet my codecs return string containing something like: Network 1 Ethernet MacAddress: “E4:C7:11:22:33:44”. So I search for first quote and return next 17 symbols

4) So in the main part let’s iterate through  hosts list and just print IP address along with MAC

if __name__ == "__main__":
    for i in hosts:
    print i,', ', get_mac(i)

5) Now I’m going to put it in my CSV file and use another script, which uses CUCM AXL-Serviceability API to add devices to CUCM – stay with me to get the details!

Installation of Python 2.7.9 in Ubuntu 14.04

Ubuntu 14.04 ships with python 2.7.6 and uses it a lot for it’s internal needs. But python2.7.6 lacks very usefull module – ssl (https://docs.python.org/2.7/library/ssl.html). I like ssl.create_default_context a lot – saves a lot of work when dealing with HTTPS. So here are the steps to get separate installation of python2.7.9 leaving default 2.7.6 intact 1) You have clean Ubuntu 14.04 and now it’s time to install some important packages

sudo apt-get update
sudo apt-get upgrade
sudo apt-get install build-essential
sudo apt-get install libreadline-gplv2-dev libncursesw5-dev libssl-dev libsqlite3-dev tk-dev libgdbm-dev libc6-dev libbz2-dev
sudo apt-get install checkinstall

2) Now let’s fetch python’s source and extract it to our home directory

mkdir ~/Downloads
cd ~/Downloads
wget http://python.org/ftp/python/2.7.9/Python-2.7.9.tgz
tar -xvf Python-2.7.9.tgz
cd Python-2.7.9

3) It’s time to compile to to a separate directory (say /opt/python-2.7.9)

sudo mkdir /opt/python279
./configure --prefix=/opt/python279
make

4) Here is an important step – we are not going to make install it, but use checkinstall instead. It will create deb package and install it (so it can be easily removed later if needed, for example). We will provide custom package (python-2.7.9) name to identify it later.

sudo checkinstall --pkgname python-2.7.9

5) That’s it. Type /opt/python279/bin/python -V and it will show the version 6) You can delete it later with sudo dpkg -r python-2.7.9