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
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 (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:
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:
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.
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):
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/
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.
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
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.
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.
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
17) Move to Cisco TCSadministration 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.
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.
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).
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.
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']
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!
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
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