├── .gitignore ├── .travis.yml ├── AUTHORS ├── CHANGELOG.md ├── CONTRIBUTING.md ├── COPYING ├── MANIFEST.in ├── README.md ├── RELEASE.md ├── VERSION ├── docs ├── Makefile ├── README ├── Task.rst ├── cii-client.rst ├── cii-messages.rst ├── cii-overview.rst ├── cii-server.rst ├── cii.rst ├── clock.rst ├── conf.py ├── correlated-clock-speed-change-issue.graffle ├── correlated-clock-speed-change-issue.png ├── correlated-clock.graffle ├── correlated-clock.png ├── examples.rst ├── index.rst ├── internals.rst ├── make.bat ├── monotonic_time.rst ├── protocol.rst ├── range-correlated-clock.graffle ├── range-correlated-clock.png ├── servers-internals.rst ├── task-internals.rst ├── timing.rst ├── ts-client.rst ├── ts-messages.rst ├── ts-overview.rst ├── ts-server.rst ├── ts.rst ├── wc-client-clock-model.graffle ├── wc-client-clock-model.png ├── wc-client.rst ├── wc-messages.rst ├── wc-overview.rst ├── wc-request-response.graffle ├── wc-request-response.png ├── wc-server.rst └── wc.rst ├── dvbcss ├── __init__.py ├── clock.py ├── monotonic_time.py ├── protocol │ ├── __init__.py │ ├── cii.py │ ├── client │ │ ├── __init__.py │ │ ├── cii.py │ │ ├── ts.py │ │ └── wc │ │ │ ├── __init__.py │ │ │ └── algorithm │ │ │ ├── __init__.py │ │ │ ├── _dispersion.py │ │ │ ├── _filterpredict.py │ │ │ └── _simple.py │ ├── server │ │ ├── __init__.py │ │ ├── cii.py │ │ ├── ts.py │ │ └── wc.py │ ├── transformers.py │ ├── ts.py │ └── wc.py ├── task.py └── util.py ├── examples ├── CIIClient.py ├── CIIServer.py ├── TSClient.py ├── TSServer.py ├── TVDevice.py ├── WallClockClient.py ├── WallClockServer.py ├── __init__.py ├── _useDvbCssUninstalled.py └── clocksAndTasks.py ├── requirements.txt ├── setup.py └── test ├── __init__.py ├── _useDvbCssUninstalled.py ├── mock_dependent.py ├── mock_time.py ├── test_Clock.py ├── test_Task.py ├── test_all.py ├── test_cii.py ├── test_monotonic_time.py ├── test_ts.py └── test_wc.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # text editor backups 6 | *~ 7 | 8 | # Eclipse 9 | .project 10 | .settings 11 | .externalToolBuilders 12 | .pydevproject 13 | 14 | # svn 15 | .svn 16 | 17 | # Mac OS X 18 | .DS_STORE 19 | .DS_Store 20 | 21 | # sphinx-doc 22 | build 23 | docs/_build 24 | docs/_static 25 | 26 | # Distribution / packaging 27 | .Python 28 | env/ 29 | build/ 30 | develop-eggs/ 31 | dist/ 32 | downloads/ 33 | eggs/ 34 | lib/ 35 | lib64/ 36 | parts/ 37 | sdist/ 38 | var/ 39 | *.egg-info/ 40 | .installed.cfg 41 | *.egg 42 | 43 | # Installer logs 44 | pip-log.txt 45 | pip-delete-this-directory.txt 46 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | os: 4 | # - osx ... not yet supported 5 | - linux 6 | 7 | python: 8 | - "2.7" 9 | 10 | install: 11 | - pip install -r requirements.txt 12 | - python setup.py install 13 | 14 | script: 15 | - py.test 16 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | British Broadcasting Corporation 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## Latest 4 | 5 | # 0.5.2 : pypi packaging bugfix 6 | 7 | This is a minor release that fixes a packaging bug that may cause pydvbcss 8 | to be unable to install correctly unless CherryPy and ws4py packages are 9 | already installed. 10 | 11 | * Fixed missing `requirements.txt` in pypi package 12 | ([c696395](https://github.com/bbc/pydvbcss/commit/c6963955f87e9569b6de689697ad95fe182e1893)) 13 | 14 | 15 | # 0.5.1 : Installation bugfixes 16 | 17 | This is a minor release containing a temporary workaround for installation 18 | problems caused by changes to packages this library depends on (cherrypy). 19 | 20 | * Temporary fix for bug due to changes in cherrypy 21 | ([f5bb1b1](https://github.com/bbc/pydvbcss/commit/f5bb1b17653f4b431adadb0ad7f819cd4c444640) and 22 | [7ad71e9](https://github.com/bbc/pydvbcss/commit/7ad71e9c96f4732666725455b30767e9daa891cd)). 23 | Will be reverted once resolved by dependencies 24 | 25 | 26 | ## 0.5.0 : CII server improvement and fix for ws4py 0.3.6 27 | 28 | This is a minor release containing mostly maintenance fixes arising from 29 | recent changes to cherrypy/ws4py, some small documentation improvements, 30 | and an enhancement to CII server support for when bound to multiple 31 | network interfaces. 32 | 33 | * Enhancement: CII server can transparently rewrite `wcUrl`, `tsUrl` and 34 | `teUrl` to take account of the network interface the client has connected 35 | to - enabling it to work when bound to multiple interfaces. 36 | ([#14](https://github.com/bbc/pydvbcss/issues/14)) 37 | * Docs: `README.md` improvements. 38 | * Switch from temporary to permanent fix due to cherrypy/ws4py bugs that have now been 39 | fixed. ([#13](https://github.com/bbc/pydvbcss/issues/13)) 40 | 41 | ## 0.4.1 : Bugfixes and new clock object type 42 | 43 | This is a minor release that includes a temporary workaround for a problem 44 | ith dependencies (cherrypy and ws4py) when installing. It also includes 45 | various bug fixes and a new type of Clock object that is useful when 46 | compensating for rendering latencies. 47 | 48 | * Bugfix: fixed semantics on `calcWhen()`, `toParenTicks()` and `toRootTicks()` 49 | when clock speed is 0.` ([32256b6](https://github.com/bbc/pydvbcss/commit/32256b6f94ce01466ab4645097672e58a00456cc)) 50 | * API addition: new `OffsetClock` class to make it easy to compensate for rendering latencies. 51 | * Temporary fix for bug due to changes in cherrypy ([f8ec3f6](https://github.com/bbc/pydvbcss/commit/f8ec3f665896d8cc90c5963781e00d366c9d8331)). Will be reverted once resolved by dependencies. 52 | 53 | ## 0.4.0 : Bugfixes and overhaul of clock object model (new-clock-model) 54 | 55 | This release contains a significant internal upgrade to the `dvbcss.clock` 56 | module with minor knock-on effects on other packages - particularly wall clock 57 | client and server code and algorithms. The changes have been implemented to 58 | be mostly backwardly compatible, so existing code should continue to work. The 59 | only exception will be any custom wall clock client algorithms. 60 | 61 | Clock objects can now calculate **dispersion** (error bounds). Clocks can also 62 | track clock **availability** (mirroring the concept of timeline availability). 63 | 64 | Wall clock client algorithms have been switched from measuring and adjusting 65 | the same clock (representing the wall clock) to instead measuring its parent 66 | and setting the correlation. Any custom wall clock client algorithms will 67 | not work and will need to be updated. 68 | 69 | #### How to 'upgrade' existing code 70 | 71 | * Use a `CorrelatedClock` instead of a `TunableClock` to model a wall clock. 72 | and set the "maximum frequency error" when initialising the `SysClock` 73 | instead of passing it to a `WallClockClient` or `WallClockServer`. 74 | 75 | * Use a `Correlation` object instead of a tuple (parentT,childT) to represent 76 | correlations for a `CorrelatedClock` 77 | 78 | * Control and check timeline availability of clock objects instead of setting 79 | or querying the availability of the TS protocol server or client. 80 | 81 | * Update any custom algorithms you might have created for wall clock clients. 82 | 83 | * `Candidate` objects now represent the relationship between the **parent** 84 | of the local wall clock (instead of the local wall clock itself) 85 | and the server's wall clock.` 86 | 87 | * Algorithms and Filters now receive a single `Candidate` object in units of 88 | nanoseconds. They no longer receive a `Candidate` converted to units of 89 | clock ticks. 90 | 91 | * Predictors should return a `Correlation` instead of an adjustment value. 92 | 93 | * `DispersionCalculator` is being deprecated. It still exists, but you should 94 | stop using it and instead use `Candidate.calcCorrelationFor` function to 95 | create a correlation which you then put into a `CorrelatedClock` and call 96 | the `dispersionAtTime` function. 97 | 98 | Examples of old way (0.3.x and earlier): 99 | 100 | s = SysClock() 101 | wallClock = TunableClock(s, tickRate=1000000000) 102 | 103 | algorithm = LowestDispersionCandidate(wallClock,repeatSecs=1,timeoutSecs=0.5, localMaxFreqErrorPpm=500) 104 | wc_client=WallClockClient(bind, dest, wallClock, algorithm) 105 | wc_client.start() 106 | 107 | timeline = CorrelatedClock(wallClock, tickRate=1000, (10,20)) 108 | 109 | timeline.correlation = ( timeline.correlation[0], timeline.correlation[1] + 50) 110 | 111 | ts = TSClientClockController(tsUrl, contentIdStem, timelineSelector, timeline) 112 | print ts.available 113 | 114 | Equivalent new way (0.4 and later): 115 | 116 | s=SysClock(maxFreqErrorPpm=500) 117 | wallClock=CorrelatedClock(s,tickRate=1000000000) 118 | 119 | algorithm = LowestDispersionCandidate(wallClock,repeatSecs=1,timeoutSecs=0.5) 120 | wc_client=WallClockClient(bind, dest, wallClock, algorithm) 121 | wc_client.start() 122 | 123 | timeline = CorrelatedClock(wallClock, tickRate=1000, Correlation(10,20)) 124 | 125 | timeline.correlation = timeline.correlation.butWith(childTicks=timeline.childTicks + 50) 126 | 127 | ts = TSClientClockController(tsUrl, contentIdStem, timelineSelector, timeline) 128 | print timeline.isAvailable() 129 | 130 | #### Summary of changes: 131 | 132 | Main changes in new clock model ([da846a9](https://github.com/bbc/pydvbcss/commit/da846a96ec8dd3e23b4a3e363fd98d1c495cc8c5)): 133 | * API addition: can setParent() on `CorrelatedClock`, `RangeCorrelatedClock` and `TunableClock`. 134 | * API change: All clock objects modified to be able to track error values and calculate dispersions and clock `availability` 135 | * API change: `CorrelatedClock` class modified to use a `Correlation` object instead of a tuple. 136 | * API change: `TunableClock` reimplemented as subclass of `CorrelatedClock` 137 | * API change: `WallClockClient`, `WallClockServer` initialisation arguments - precision and maxfreqerror now optional. Now, by default, taken from the clock. 138 | * API change: Wall clock client algorithms (dispersion, filtering, prediction) changed to update a CorrelatedClock and to measure the parent of the clock representing the wall clock. Review the documentation to understand how to update any algorithms you might have implemented. 139 | * API change: `DispersionCalculator` class deprecated (but still available). Use `Candidate.calcCorrelationFor` and `CorrelatedClock.dispersionAtTime` instead. 140 | * Tests: Updated to reflect changes. 141 | * Docs: Updated to reflect changes. 142 | 143 | Other improvements/bugfixes: 144 | * Bugfix: Tracking of number of connections to CSS-CII and CSS-TS had a leak ([4c76042](https://github.com/bbc/pydvbcss/commit/4c76042d2e6c69f2c38682468738ba8cca02b5d1)) 145 | * API addition: `setParent()` on `CorrelatedClock` ([d4576d7](https://github.com/bbc/pydvbcss/commit/d4576d7440e8e9f5ced4e73fa182edc05442b1b8)) 146 | * Bugfix: NotImplemented exception used incorrectly ([d4576d7](https://github.com/bbc/pydvbcss/commit/d4576d7440e8e9f5ced4e73fa182edc05442b1b8)) 147 | * Bugfix: Workaround for OSX 10.11 SIP for monotonic clock module ([P-R #6](https://github.com/bbc/pydvbcss/pull/6) and [P-R #7](https://github.com/bbc/pydvbcss/pull/7)) 148 | * Improvement: Support for monotonic clock on Android. 149 | ([P-R #5](https://github.com/bbc/pydvbcss/pull/5) by [Jack Jansen](https://github.com/jackjansen)) 150 | * Bugfix: `clock.getEffectiveSpeed()` stuck in infinite loop. ([ceb3f33](https://github.com/bbc/pydvbcss/commit/ceb3f33edb7b94359ecb2cac74046b92b2cc5094)) 151 | * Docs: Now correctly links to github source for correct branch/version. 152 | * Docs: Improvements to setup/release process to format PyPI description correctly (by converting to ReStructuredText) 153 | 154 | ## 0.3.3 : Bugfixes and thread safety improvements (20 Apr 2016) 155 | 156 | * Build: Migrated to hosting documentation on readthedocs.org and doing build checks with travis-ci.org 157 | * Bugfix: improvements to thread safety 158 | * Bugfix: incorrect arguments on disconnect handler in CIIClient 159 | 160 | ## 0.3.2 : Bugfixes and minor API enhancements (17 Dec 2015) 161 | 162 | * Bugfix: Initialiser for `CIIServer` class to avoid corruption of default value for `initialCII` if a 2nd CII server is instantiated 163 | * API addition: Added `copy()` methods to all classes representing JSON messages. 164 | * Bugfix: `TSClient` class did not correctly handle when contentId is null 165 | * Bugfix: `examples/TSClient.py` now exits when the connection is closed. 166 | 167 | ## 0.3.1 : Packaging version fix (01 Sep 2015) 168 | 169 | * Completed support for uploading packages to pypi to enable installation using `pip` 170 | * Bugfix: setup.py was not tracking same version number as in `VERSION` file. 171 | 172 | ## 0.3 : Bugfixes and CSS-TS Server API enhancements (01 Sep 2015) 173 | 174 | * API change: Modified API for TimelineSource for TS Servers to allow for situations where it takes time to begin extracting a timeline at the server. 175 | * Docs: Switched to referring to spec as ETSI 103 286 now it is published by ETSI 176 | * Docs: Updated to reflect API changes 177 | * Docs: General minor fixes 178 | * Bugfix: CIIServer disconnect callback failed due to wrong number of arguments 179 | * Bugfix: Incorrect use of `"presentationStatus"` CII property in examples. Should have been a list, not a string. Also added a check to trap this. 180 | * Bugfix: Some command line arguments being ignored in `examples/TVDevice.py` 181 | * Bugfix: Wallclock time printed in wrong units in `examples/WallClockClient.py` 182 | 183 | ## 0.2 : Client dispersion reporting release (17 Mar 2015) 184 | 185 | * API change: Added ways to extract dispersion and worst-dispersion information more usefully from wall clock clients. 186 | * Tests: Minor unit test improvements 187 | 188 | ## 0.1.1 : Bugfix release (18 Feb 2015) 189 | 190 | * Bugfix: Fixed problem in `setup.py` that meant when pydvbcss is installed it could not be imported into another python program 191 | * Bugfix: Fixed incorrect content ID URLs used in CII Server example 192 | * Docs: Various documentation improvements 193 | * Tests: New unit tests 194 | 195 | ## 0.1 : initial release 196 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to pydvbcss 2 | 3 | *pydvbcss* is licensed under the Apache Licence 2.0. 4 | 5 | ## Small patches and bug fixes 6 | 7 | We want to make contribution of small patches and bug fixes as easy as possible. The Apache v2.0 licence makes it straight forward to accept such contributions. Clause 5 of Apache v2.0 permits us to include your patches in return for copyright acknowledgement in the AUTHORS file. 8 | 9 | ## New features and improvements 10 | 11 | If you would like to make a more substantial contribution thats great! But first, we ask that you please get in touch and we'll sort out a simple *Contribution Agreement*. 12 | 13 | The purpose of such an agreement is to define the terms on which you contribute your work and lets us know that everything you contribute is your own. This ensures it can safely be included under the terms of the Apache Licence, Version 2.0, thus protecting the project and anyone who uses it. 14 | 15 | ## Fork, commit, submit 16 | 17 | If you are happy to contribute under the terms described above, then please follow the normal conventions for github... 18 | 19 | 1. Fork it 20 | 2. Commit your changes 21 | 3. Submit a pull request 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include dvbcss *.py 2 | recursive-include test *.py 3 | include setup.py setup.cfg 4 | include README.md 5 | include VERSION 6 | include requirements.txt 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python DVB Companion Screen Synchronisation protocol library and tools 2 | 3 | [![Build status](https://travis-ci.org/bbc/pydvbcss.svg?branch=master)](https://travis-ci.org/bbc/pydvbcss) 4 | [![Docs Status (stable)](https://readthedocs.org/projects/pydvbcss/badge/?version=stable)](http://pydvbcss.readthedocs.io/en/stable/?badge=stable) 5 | [![Docs Status (latest)](https://readthedocs.org/projects/pydvbcss/badge/?version=latest)](http://pydvbcss.readthedocs.io/en/latest/?badge=latest) 6 | [![Latest PyPI package](https://img.shields.io/pypi/v/pydvbcss.svg)](https://pypi.python.org/pypi/pydvbcss) 7 | 8 | * **[How to install](#install-the-code)** 9 | * **[Read the documentation](#read-the-documentation)** 10 | * **[Run the examples](#run-the-examples)** 11 | 12 | **pydvbcss** is a set of Python 2.7 libraries and command-line tools that implement some of the 13 | protocols defined in the DVB CSS specification (published as [ETSI 103-286 part 2](http://www.etsi.org/standards-search?search=103+286&page=1&title=1&keywords=1&ed=1&sortby=1)) 14 | and are used for the "inter-device synchronisation" feature in **[HbbTV 2](http://hbbtv.org/resource-library/)**. 15 | These protocols enable synchronisation of media presentation between a TV 16 | and Companion devices (mobiles, tablets, etc). 17 | 18 | This library includes simple to use high level abstractions that wrap up the 19 | server or client behaviour for each protocol as well as low level code for 20 | packing and unpacking messages sent across the protocols. There are also 21 | objects that work with the rest of the library to represent clocks and timelines. 22 | 23 | This code is intended as an informal reference and is suitable for building 24 | prototypes and testing tools that implement TV (server) or Companion 25 | (client) behaviour. It is not considered production ready or suitable for 26 | integration into consumer products. 27 | 28 | The code does not implement media playback functionality and this is not a planned 29 | feature. 30 | 31 | The DVB CSS specification was formerly published as [DVB Bluebook A167-2](https://www.dvb.org/search/results/keywords/A167). This is deprecated in favour of the [ETSI spec](http://www.etsi.org/standards-search?search=103+286&page=1&title=1&keywords=1&ed=1&sortby=1). 32 | 33 | ## Getting started 34 | 35 | **pydvbcss** requires [ws4py](https://ws4py.readthedocs.io/en/latest/) for 36 | use in clients and servers, and also [cherrypy](http://www.cherrypy.org) 37 | for server implementations. The steps below describe how to install these. 38 | 39 | **pydvbcss** has been developed on Mac OS X 10.10 but has also been used 40 | successfully on Microsoft Windows 7 and Ubuntu 14.04. 41 | 42 | 43 | 44 | ### Read the Documentation 45 | 46 | The docs for the library can be read online on readthedocs.org: 47 | 48 | * [![Docs Status (stable)](https://readthedocs.org/projects/pydvbcss/badge/?version=stable)](http://pydvbcss.readthedocs.io/en/stable/?badge=stable) [Docs for current stable release](http://pydvbcss.readthedocs.io/en/stable/?badge=stable) 49 | 50 | * [![Docs Status (latest)](https://readthedocs.org/projects/pydvbcss/badge/?version=latest)](http://pydvbcss.readthedocs.io/en/latest/?badge=latest) [Docs for latest commits to master release](http://pydvbcss.readthedocs.io/en/latest/?badge=latest) 51 | 52 | Links are also available from those pages through to documentation for earlier releases. 53 | 54 | 55 | 56 | ### Install the code ... 57 | 58 | *On Mac OS X and Linux you may need to run one or more of the commands as root.* 59 | 60 | #### Using PyPi _(core library only, no examples or tools)_ 61 | 62 | * [![Latest PyPI package](https://img.shields.io/pypi/v/pydvbcss.svg)](https://pypi.python.org/pypi/pydvbcss) [See the pydvbcss PyPI package page](https://pypi.python.org/pypi/pydvbcss). 63 | 64 | If you ONLY want the library (not the [code examples and tools](#run-examples) ) and 65 | if you don't require the very latest bugfixes, then you can install a recent 66 | release package from the Python Package Index (PyPI) using 67 | [pip](https://pip.pypa.io/en/latest/installing.html): 68 | 69 | $ pip install pydvbcss 70 | 71 | Or if upgrading from a previous version: 72 | 73 | $ pip install --upgrade pydvbcss 74 | 75 | You can use `pip search pydvbcss` to verify which version is installed. 76 | 77 | > *See note in the next section about `CherryPy` and `ws4py` dependencies.* 78 | 79 | 80 | #### From Github or a release tarball _(includes examples and tools)_ 81 | 82 | The [master branch](https://github.com/BBC/pydvbcss/tree/master) is the latest 83 | state of the code, including any recent bugfixes. It is mostly stable but 84 | might have occasional small API changes. 85 | [Release snapshots](https://github.com/BBC/pydvbcss/releases) are also available 86 | but won't contain the very latest bugfixes or new features. 87 | Both of these options include the full code, including [examples](#run-examples). 88 | 89 | First you need to install dependencies... 90 | 91 | We recommend using [pip](https://pip.pypa.io/en/latest/installing.html) to install 92 | dependencies from the Python Package Index [PyPI](https://pypi.python.org/pypi): 93 | 94 | $ pip install -r requirements.txt 95 | 96 | > *NOTE: There have been recent incompatibilities between certain versions of 97 | > `cherrypy`, `ws4py` and `cheroot`. Therefore, `requirements.txt` requires specific 98 | > (older) versions of these pacakges. You are welcome to try newer versions installing 99 | > them manually. See [#15](https://github.com/bbc/pydvbcss/issues/15) for background 100 | > details.* 101 | 102 | Then take (or update) your clone of the repository *master* branch, or 103 | download and unzip a snapshot release and run the `setup.py` script to 104 | install: 105 | 106 | $ python setup.py install 107 | 108 | This will install all module packages under 'dvbcss'. 109 | 110 | There is a limited test suite (it only tests certain classes at the moment). 111 | Run it via setup.py: 112 | 113 | $ python setup.py test 114 | 115 | This checks some timing sensitive implementation issues, so ensure you are not 116 | running any CPU intensive tasks at the time. 117 | 118 | 119 | 120 | ## Running the examples and tools 121 | 122 | There is a set of example and tools demonstrating simple servers and clients for the 123 | protocols included with the library. See the 124 | [quick start guide](https://BBC.github.io/pydvbcss/docs/latest/examples.html) 125 | in the documentation to see how to run them. 126 | 127 | The clients are useful tools to test a TV implementation is outputting the correect data. 128 | 129 | The servers can be modified to simulate a TV that is playing content with an ID 130 | and timeline(s) that a companion application expects. 131 | 132 | ### Example: checking protocols implemented by a TV 133 | 134 | Start the content playing on the TV and ensure it is serving the protocols (for HbbTV 2 135 | TVs this requires an HbbTV application to enable inter-device synchronisation). 136 | 137 | Suppose the TV is serving the CII protocol at the URL `ws://192.168.0.57:7681/cii`... 138 | 139 | To check the CII protocol: 140 | 141 | $ python examples/CIIClient.py ws://192.168.0.57:7681/cii 142 | 143 | Suppose that the messages returned report the URL of the TS protocol endpoint as being `ws://192.168.0.57:7681/ts` and the wall clock protocol as being at `192.168.0.57` port `6677`... 144 | 145 | To check the TV's Wall Clock protocol:` 146 | 147 | $ python examples/WallClockClient.py 192.168.0.57 6677 148 | 149 | To check the TV reporting a PTS timeline (uses both Wall Clock and TS protocols): 150 | 151 | $ python examples/TSClient.py ws://192.168.0.57:7681/ts \ 152 | udp://192.168.0.57:6677 \ 153 | "" \ 154 | "urn:dvb:css:timeline:pts" \ 155 | 9000 156 | 157 | 158 | ## Super-quick introduction to the protocols 159 | 160 | DVB has defined 3 protocols for communicating between a companion and TV in 161 | order to create synchronised second screen / dual screen / companion 162 | experiences (choose whatever term you prefer!) that are implemented here: 163 | 164 | * CSS-CII - A WebSockets+JSON protocol that conveys state from the TV, such 165 | as the ID of the content being shown at the time. It also carries the URLs 166 | to connect to the other two protocols. 167 | 168 | * CSS-WC - A simple UDP protocol (like NTP but simplified) that establishes 169 | a common shared clock (a "wall clock") between the TV and companion, 170 | compensating for network delays. 171 | 172 | * CSS-TS - Another WebSockets+JSON protocol that communicates timestamps 173 | from TV to Companion that describe the current timeline position. 174 | 175 | The TV implements servers for all 3 protocols. The Companion implements 176 | clients. 177 | 178 | There are other protocols defined in the specification (CSS-TE and CSS-MRS) that 179 | are not currently implemented by this library. 180 | 181 | 182 | ## Building the documentation for yourself 183 | 184 | You can also build the documentation yourself. It is written using the 185 | [sphinx](http://www.sphinx-doc.org) documentation build system. 186 | 187 | Building the documentation requires [sphinx](http://www.sphinx-doc.org) and 188 | the sphinx "read the docs" theme. The easiest way is using PyPI: 189 | 190 | $ pip install sphinx 191 | $ pip install sphinx_rtd_theme 192 | 193 | The `docs` directory contains the configuration and main documentation 194 | sources that descibe the structure. Most of the actual words are in the 195 | inline docstrings in the source code. These structural pages pull these in. 196 | 197 | To build docs in HTML format, either: 198 | 199 | $ python setup.py build_sphinx 200 | 201 | or: 202 | 203 | $ cd docs 204 | $ make html 205 | 206 | 207 | 208 | ## Contact and discuss 209 | 210 | Discuss and ask questions on the [pydvbcss google group](). 211 | 212 | The original author is Matt Hammond 'at' bbc.co.uk 213 | 214 | 215 | 216 | ## Licence 217 | 218 | All code and documentation is licensed under the Apache License v2.0. 219 | 220 | 221 | 222 | ## Contributing 223 | 224 | If you would like to contribute to this project, see 225 | [CONTRIBUTING](CONTRIBUTING.md) for details. 226 | 227 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release process for pydvbcss 2 | 3 | ## Explanation of the process sequence 4 | 5 | Assumption that the current state of *master* will constitute the release... 6 | 7 | #### 1. Pre-release checks 8 | 9 | Make the following checks before performing a release: 10 | * Do all unit tests pass? 11 | * Do all examples work? 12 | * Does documentation build? 13 | 14 | 15 | #### 2. Update VERSION and CHANGELOG and README 16 | 17 | ##### Increment the version number. 18 | 19 | The version number is in the [`VERSION`](VERSION) file. This is picked up by the documentation build process. 20 | 21 | It consists of two lines. The first carries the version number. The structure is: *major* **.** *minor* **.** *revision*. 22 | The *revision* part is *not included* if it is zero '0' (just after a *major* or *minor* increment). 23 | * *major* = significant incompatible change (e.g. partial or whole rewrite). 24 | * *minor* = some new functionality or changes that are mostly/wholly backward compatible. 25 | * *revision* = very minor changes, e.g. bugfixes. 26 | 27 | The 2nd line carries the state - whether this is the "latest" code in the master branch, or whether it is a "release". 28 | Leave this as "latest" for the moment. 29 | 30 | 31 | ##### Update the change log 32 | 33 | The is in the [`CHANGELOG.md`](CHANGELOG.md) file. Ensure it mentions any noteworthy changes since the previous release. 34 | 35 | 36 | ##### Update README 37 | 38 | Ensure the README reflects any changes. 39 | 40 | 41 | 42 | #### 3. Create release branch 43 | 44 | Create the branch, naming it after the release version number (just the number). 45 | 46 | #### 4. Update VERSION and `README.md` for the release branch 47 | 48 | In the branch, now modify (and commit) the VERSION file, changing "latest" to "release". 49 | 50 | In ['README.md'](README.md) change references to the "master" branch to the name 51 | of release branch that will be used in the next step (which will be the 52 | release version number). The places this occurs includes: 53 | 54 | * the Travis CI build status image link at the beginning. 55 | * The "stable" docs links at the beginning and in the docs section. Rewrite them to be named after the version number 56 | 57 | 58 | #### 5. Create a new release on GitHub based on the new branch 59 | 60 | Put a shorter summary of the new changelog items into the release notes. Make the tag name the version number 61 | - the same as the branch name. 62 | 63 | 64 | #### 6. Check documentation builds 65 | 66 | _The old process of manually building and pushing to gh-pages is deprecated._ 67 | 68 | Docs are built automatically by readthedocs.org when the new release tag is generated. Check that the new release 69 | has built correctly and is classified as the "stable" build, here: https://readthedocs.org/projects/pydvbcss/ 70 | 71 | 72 | 73 | #### 7. Upload new package to python package index 74 | 75 | The process originally followed to register and setup first time was [this one](http://peterdowns.com/posts/first-time-with-pypi.html). 76 | 77 | For subsequent releases, do an upload to first *PyPI Test* and only if that succeeds then do an upload to *PyPI Live*. 78 | 79 | - - - - - 80 | 81 | ## Example of release process sequence 82 | 83 | This example assumes your local repository is a clone and the working copy is currently at the head of the master branch, and that this is all 84 | synced with GitHub. The following steps will do a release "X.Y.Z" 85 | 86 | $ git status 87 | On branch master 88 | Your branch is up-to-date with 'origin/master'. 89 | nothing to commit, working directory clean 90 | 91 | ### 1. Run checks 92 | 93 | Run unit tests: 94 | 95 | $ python tests/test_all.py 96 | 97 | Also check the documentation builds: 98 | 99 | $ python setup.py build_sphinx 100 | 101 | ... and manually review areas where it will have changed 102 | 103 | And run all examples and check they work! 104 | 105 | 106 | 107 | ### 2. Update VERSION and CHANGELOG 108 | 109 | Update the version number in `master`, e.g. using `vi`: 110 | 111 | $ vi VERSION 112 | .. change version number line only .. 113 | $ vi CHANGELOG.md 114 | .. update change log .. 115 | $ git add VERSION 116 | $ git add CHANGELOG.md 117 | $ git commit -m "Version number increment and Changelog update ready for release" 118 | 119 | And push to GitHub: 120 | 121 | $ git push origin master 122 | 123 | ### 3. Create release branch 124 | 125 | Create new branch (locally) 126 | 127 | $ git checkout -b 'X.Y.Z' 128 | 129 | Update VERSION to mark as "release" within the branch 130 | 131 | $ vi VERSION 132 | .. change "latest" to "release" 133 | 134 | Update README.md to change: 135 | 136 | * travis CI build status to be specific for this version branch 137 | * "stable" docs links to point to this version branch specifically. 138 | 139 | Commit changes: 140 | 141 | $ git add VERSION README.md 142 | $ git commit -m "Version marked as release." 143 | 144 | Push branch up to github (and set local repository to track the upstream branch on origin): 145 | 146 | $ git push -u origin 'X.Y.Z' 147 | 148 | 149 | ### 4. Create a new release on GitHub based on the new branch 150 | 151 | Now use the [new release](https://github.com/bbc/pydvbcss/releases/new) function on GitHub's web interface to 152 | mark the branch 'X.Y.Z' as a new release. 153 | 154 | ### 5. Update documentation builds on gh-pages (deprecated) 155 | 156 | Documentation is now automatically rebuilt and hosted by readthedocs.org. It 157 | will be picked up when the new release is tagged. 158 | 159 | 160 | ### 6. Upload to PyPI: 161 | 162 | To upload, you must have [pandoc](http://pandoc.org/) installed as a command 163 | line tool. This is needed to convert the README from [Markdown](https://daringfireball.net/projects/markdown/) 164 | to [ReStructuredText](http://docutils.sourceforge.net/docs/ref/rst/introduction.html) because PyPI 165 | requires it. 166 | 167 | $ sudo apt-get install pandoc # Debian/ubuntu Linux 168 | $ sudo port install pandoc # Mac Ports 169 | 170 | ... first uploading to the test service to check everything is okay: 171 | 172 | $ git checkout <> 173 | $ sudo python setup.py sdist register upload -r pypitest 174 | 175 | ... then going live: 176 | 177 | $ sudo python setup.py sdist register upload -r pypi 178 | 179 | The conversion of the README alone can be checked by doing a 'register' without 180 | an 'upload'. 181 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.5.2 2 | -latest 3 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/PythonDVBSynccode.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PythonDVBSynccode.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/PythonDVBSynccode" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PythonDVBSynccode" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/README: -------------------------------------------------------------------------------- 1 | The latest build of this documentation can be read online at: 2 | 3 | https://BBC.github.io/pydvbcss/docs/latest/ 4 | 5 | The documentation is built using the sphinx documentation build system. 6 | To build it yourself, see README.md in the parent directory. 7 | -------------------------------------------------------------------------------- /docs/Task.rst: -------------------------------------------------------------------------------- 1 | .. py:module:: dvbcss.task 2 | 3 | ========================== 4 | Task scheduling for clocks 5 | ========================== 6 | 7 | Module: `dvbcss.task` 8 | 9 | .. contents:: 10 | :local: 11 | :depth: 2 12 | 13 | 14 | Introduction 15 | ------------ 16 | 17 | .. automodule:: dvbcss.task 18 | :noindex: 19 | 20 | See :doc:`task-internals` for information on how the internals of the Task module work. 21 | 22 | Example 23 | ------- 24 | 25 | A simple example: 26 | 27 | .. code-block:: python 28 | 29 | from dvbcss.clock import SysClock 30 | from dvbcss.clock import CorrelatedClock 31 | from dvbcss.task import sleepFor, runAt 32 | 33 | s = SysClock() 34 | c = CorrelatedClock(parentClock=s, tickRate=1000) 35 | 36 | # wait 1 second 37 | sleepFor(c, numTicks=1000) 38 | 39 | # schedule callback in 5 seconds 40 | def foo(message): 41 | print "Callback!", message 42 | 43 | runAt(clock=c, whenTicks=c.ticks+5000, foo, "Tick count progressed by 5 seconds") 44 | 45 | # ... but change the correlation to make the clock jump 1 second forward 46 | # causing the callback to happen one second earlier 47 | c.correlation = (c.correlation[0], c.correlation[1] + 1000) 48 | 49 | # ... the callback will now happen in 4 seconds time instead 50 | 51 | 52 | Functions 53 | --------- 54 | 55 | .. autofunction:: dvbcss.task.sleepUntil 56 | 57 | .. autofunction:: dvbcss.task.sleepFor 58 | 59 | .. autofunction:: dvbcss.task.scheduleEvent 60 | 61 | .. autofunction:: dvbcss.task.runAt 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /docs/cii-client.rst: -------------------------------------------------------------------------------- 1 | .. py:module:: dvbcss.protocol.client.cii 2 | 3 | =============== 4 | CSS-CII Clients 5 | =============== 6 | 7 | Module: `dvbcss.protocol.client.cii` 8 | 9 | .. contents:: 10 | :local: 11 | :depth: 2 12 | 13 | .. automodule:: dvbcss.protocol.client.cii 14 | :noindex: 15 | 16 | Classes 17 | ------- 18 | 19 | **CIIClient** 20 | ''''''''''''' 21 | 22 | .. autoclass:: CIIClient 23 | :members: 24 | :exclude-members: cii, latestCII, connected 25 | :inherited-members: 26 | 27 | .. autoinstanceattribute:: connected 28 | :annotation: 29 | 30 | .. autoinstanceattribute:: cii 31 | :annotation: 32 | 33 | .. autoinstanceattribute:: latestCII 34 | :annotation: 35 | 36 | 37 | 38 | **CIIClientConnection** 39 | ''''''''''''''''''''''' 40 | 41 | .. autoclass:: CIIClientConnection 42 | :members: 43 | :inherited-members: 44 | 45 | -------------------------------------------------------------------------------- /docs/cii-messages.rst: -------------------------------------------------------------------------------- 1 | .. py:module:: dvbcss.protocol.cii 2 | 3 | ======================= 4 | CSS-CII Message objects 5 | ======================= 6 | 7 | Module: `dvbcss.protocol.cii` 8 | 9 | .. contents:: 10 | :local: 11 | :depth: 2 12 | 13 | .. automodule:: dvbcss.protocol.cii 14 | :noindex: 15 | 16 | Classes 17 | ~~~~~~~ 18 | 19 | CII Message 20 | ----------- 21 | 22 | .. autoclass:: CII 23 | :members: 24 | :exclude-members: protocolVersion, mrsUrl, contentId, contentIdStatus, presentationStatus, wcUrl, tsUrl, teUrl, timelines, private 25 | :inherited-members: 26 | 27 | .. autoinstanceattribute:: protocolVersion 28 | :annotation: = OMIT 29 | 30 | .. autoinstanceattribute:: mrsUrl 31 | :annotation: = OMIT 32 | 33 | .. autoinstanceattribute:: contentId 34 | :annotation: = OMIT 35 | 36 | .. autoinstanceattribute:: contentIdStatus 37 | :annotation: = OMIT 38 | 39 | .. autoinstanceattribute:: presentationStatus 40 | :annotation: = OMIT 41 | 42 | .. autoinstanceattribute:: wcUrl 43 | :annotation: = OMIT 44 | 45 | .. autoinstanceattribute:: tsUrl 46 | :annotation: = OMIT 47 | 48 | .. autoinstanceattribute:: teUrl 49 | :annotation: = OMIT 50 | 51 | .. autoinstanceattribute:: timelines 52 | :annotation: = OMIT 53 | 54 | .. autoinstanceattribute:: private 55 | :annotation: = OMIT 56 | 57 | 58 | Timeline Option 59 | --------------- 60 | 61 | .. autoclass:: TimelineOption 62 | :members: 63 | :exclude-members: timelineSelector, unitsPerTick, unitsPerSecond, accuracy, private 64 | :inherited-members: 65 | 66 | .. autoinstanceattribute:: timelineSelector 67 | :annotation: = OMIT 68 | 69 | .. autoinstanceattribute:: unitsPerTick 70 | :annotation: = OMIT 71 | 72 | .. autoinstanceattribute:: unitsPerSecond 73 | :annotation: = OMIT 74 | 75 | .. autoinstanceattribute:: accuracy 76 | :annotation: = OMIT 77 | 78 | .. autoinstanceattribute:: private 79 | :annotation: = OMIT 80 | 81 | 82 | -------------------------------------------------------------------------------- /docs/cii-overview.rst: -------------------------------------------------------------------------------- 1 | CSS-CII Protocol introduction 2 | ----------------------------- 3 | 4 | **Here is a quick introduction to the CSS-CII protocol. For full details, refer to the** 5 | DVB specification `ETSI 103 286 part 2 `_. 6 | 7 | The CSS-CII protocol is for sharing the server's (e.g. TV's) current 8 | "Content Identifier and other Information" (yes really!) with the client (e.g. 9 | companion). It also includes the URL of the :doc:`CSS-TS ` and 10 | :doc:`CSS-WC ` servers so the client knows where to find them. 11 | 12 | CII comprises a set of defined properties. The server pushes state update 13 | messages containing some or all properties (at minimum those that have changed). 14 | How often these messages are pushed and which properties are included are 15 | up to the server. 16 | 17 | It is a WebSockets based protocol and messages are in JSON format. 18 | 19 | .. contents:: 20 | :local: 21 | :depth: 1 22 | 23 | 24 | Sequence of interaction 25 | ~~~~~~~~~~~~~~~~~~~~~~~ 26 | 27 | The client is assumed to already know the WebSocket URL for the CSS-CII server 28 | (for example: because the TV chooses to advertise it via a network service 29 | discovery mechanism). 30 | 31 | 1. The client connects to the CSS-TS server. Either this is refused via an HTTP 32 | status code response, or it is accepted. 33 | 34 | 2. The server immediately responds with a first CII state update message. This 35 | contains (at minimum) all properties whose values are not `null`. 36 | 37 | 3. The server can re-send the CII state update message as often as it wishes. 38 | At minimum it will do so when one or more of the properties have changed 39 | value. The server will, at minimum, include the properties that have changed, 40 | but could also include others in the message. 41 | 42 | This protocol is a state update mechanism. The client is locally 43 | mirroring the state of the TV by remembering the most recent values received 44 | for each of the properties. 45 | 46 | At the start, the client assumes all properties have the value `null`. Then, 47 | when a message is received the client updates its mirror of the TV state: 48 | 49 | * If a property is included in the message (even if its value is `null`), 50 | then this is the new value for that property. 51 | 52 | * If a property is not included in the message, then its value has not changed. 53 | 54 | Any messages sent by the client are ignored by the server. 55 | 56 | 57 | CII message properties 58 | ~~~~~~~~~~~~~~~~~~~~~~ 59 | 60 | Every message sent by the server is a CII message and consists of a single JSON 61 | object with zero, one, more or all of the following properties: 62 | 63 | * ``protocolVersion`` - currently "1.1" and must be included in the first message sent by the server after the client connects. 64 | * ``contentId`` - a URI representing the ID of the content being presented by the server. 65 | This will be a variant on a DVB URL ("dvb://") for DVB broadcast services, or the URL of the MPD for MPEG DASH streams. 66 | * ``contentIdStatus`` - whether the content Id is in its "final" form or whether it is a "partial" version until full information is available. 67 | For example: a DVB broadcast content ID might not include some elements until the TV detects certain metadata in the broadcast stream which can take a few seconds. 68 | * ``presentationStatus`` - Primarily, whether presentation of the content is "okay", "transitioning" from one piece of content to the next, or in a "fault" condition. 69 | This can be extended by suffixing space separated additional terms after the primary term. 70 | * ``mrsUrl`` - The URL of an MRS server. 71 | * ``tsUrl`` - The WebSockets URL of the CSS-TS server that a client should use if it wants to do Timeline Synchronisation. 72 | * ``wcUrl`` - The UDP URL ("udp://:`_. Check if a value is NaN like this:: 26 | 27 | >>> import math 28 | >>> math.isnan(nanValue) 29 | True 30 | 31 | Converting tick values to a parent clock or to the root clock may result in this 32 | value being returned if one or more of the clocks involved has speed zero. 33 | 34 | 35 | 36 | Functions 37 | --------- 38 | 39 | **measurePrecision** - estimate measurement precision of a clock 40 | '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' 41 | 42 | .. autofunction:: measurePrecision 43 | 44 | 45 | Classes 46 | ------- 47 | 48 | **ClockBase** - base class for clocks 49 | ''''''''''''''''''''''''''''''''''''' 50 | 51 | .. autoclass:: dvbcss.clock.ClockBase 52 | :members: 53 | :inherited-members: 54 | 55 | **SysClock** - Clock based on time module 56 | ''''''''''''''''''''''''''''''''''''''''' 57 | 58 | .. autoclass:: dvbcss.clock.SysClock 59 | :members: 60 | :inherited-members: 61 | 62 | **CorrelatedClock** - Clock correlated to another clock 63 | ''''''''''''''''''''''''''''''''''''''''''''''''''''''' 64 | 65 | .. autoclass:: dvbcss.clock.CorrelatedClock 66 | :members: 67 | :inherited-members: 68 | 69 | **OffsetClock** - A clock that is offset by a fixed amount of root time 70 | ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' 71 | 72 | .. autoclass:: dvbcss.clock.OffsetClock 73 | :members: 74 | :inherited-members: 75 | 76 | **TunableClock** - Clock with dynamically adjustable frequency and tick offset 77 | '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' 78 | 79 | .. autoclass:: dvbcss.clock.TunableClock 80 | :members: 81 | :inherited-members: 82 | 83 | **RangeCorrelatedClock** - Clock correlated to another clock 84 | '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' 85 | 86 | .. autoclass:: dvbcss.clock.RangeCorrelatedClock 87 | :members: 88 | :inherited-members: 89 | 90 | **Correlation** - represents a Correlation 91 | '''''''''''''''''''''''''''''''''''''''''' 92 | 93 | .. autoclass:: dvbcss.clock.Correlation 94 | :members: 95 | :inherited-members: 96 | 97 | 98 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Python DVB Sync code documentation build configuration file, created by 4 | # sphinx-quickstart on Sun Sep 7 14:10:39 2014. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys, re 16 | 17 | sys.path.insert(0,"../") 18 | 19 | # The version info for the project you're documenting, acts as replacement for 20 | # |version| and |release|, also used in various other places throughout the 21 | # built documents. 22 | # 23 | # The short X.Y version. 24 | F=open("../VERSION") 25 | txt = F.read().replace("\n","").replace("\r","") 26 | version, extra = re.match("^([.0-9a-zA-Z]+)-(.+)$", txt).groups() 27 | # The full version, including alpha/beta/rc tags. 28 | release = version + "-" + extra 29 | F.close() 30 | 31 | # links to source 32 | if extra=="release": 33 | source_branch = version 34 | elif extra=="latest": 35 | source_branch = "master" 36 | else: 37 | raise ValueError("Unrecognised version suffix '%s'" % extra) 38 | 39 | 40 | GITHUB_MASTER_URL = "https://github.com/BBC/pydvbcss/tree/%s" % source_branch 41 | 42 | extlinks= { 'repo': (GITHUB_MASTER_URL+"%s", None)} 43 | 44 | def linkcode_resolve(domain, info): 45 | if domain != 'py': 46 | return None 47 | if not info['module']: 48 | return None 49 | filename = info['module'].replace('.', '/') 50 | return "%s/%s.py" % (GITHUB_MASTER_URL, filename) 51 | 52 | # add some syntax extensions 53 | 54 | rst_epilog = """ 55 | .. |stub-method| replace:: *This is a stub for this method. Sub-classes should implement it.* 56 | """ 57 | 58 | # If extensions (or modules to document with autodoc) are in another directory, 59 | # add these directories to sys.path here. If the directory is relative to the 60 | # documentation root, use os.path.abspath to make it absolute, like shown here. 61 | #sys.path.insert(0, os.path.abspath('.')) 62 | 63 | # -- General configuration ------------------------------------------------ 64 | 65 | # If your documentation needs a minimal Sphinx version, state it here. 66 | #needs_sphinx = '1.0' 67 | 68 | # Add any Sphinx extension module names here, as strings. They can be 69 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 70 | # ones. 71 | extensions = [ 72 | 'sphinx.ext.autodoc', 73 | 'sphinx.ext.intersphinx', 74 | 'sphinx.ext.todo', 75 | 'sphinx.ext.coverage', 76 | 'sphinx.ext.linkcode', 77 | 'sphinx.ext.extlinks', 78 | ] 79 | 80 | autoclass_content = "both" 81 | 82 | # Add any paths that contain templates here, relative to this directory. 83 | templates_path = ['_templates'] 84 | 85 | # The suffix of source filenames. 86 | source_suffix = '.rst' 87 | 88 | # The encoding of source files. 89 | #source_encoding = 'utf-8-sig' 90 | 91 | # The master toctree document. 92 | master_doc = 'index' 93 | 94 | # General information about the project. 95 | project = u'pydvbcss' 96 | copyright = u'2015, British Broadcasting Corporation' 97 | 98 | # The language for content autogenerated by Sphinx. Refer to documentation 99 | # for a list of supported languages. 100 | #language = None 101 | 102 | # There are two options for replacing |today|: either, you set today to some 103 | # non-false value, then it is used: 104 | #today = '' 105 | # Else, today_fmt is used as the format for a strftime call. 106 | #today_fmt = '%B %d, %Y' 107 | 108 | # List of patterns, relative to source directory, that match files and 109 | # directories to ignore when looking for source files. 110 | exclude_patterns = ['_build'] 111 | 112 | # The reST default role (used for this markup: `text`) to use for all 113 | # documents. 114 | #default_role = None 115 | 116 | # If true, '()' will be appended to :func: etc. cross-reference text. 117 | add_function_parentheses = True 118 | 119 | # If true, the current module name will be prepended to all description 120 | # unit titles (such as .. function::). 121 | #add_module_names = True 122 | 123 | # If true, sectionauthor and moduleauthor directives will be shown in the 124 | # output. They are ignored by default. 125 | #show_authors = False 126 | 127 | # The name of the Pygments (syntax highlighting) style to use. 128 | pygments_style = 'sphinx' 129 | highlight_language = 'python' 130 | 131 | # A list of ignored prefixes for module index sorting. 132 | #modindex_common_prefix = [] 133 | 134 | # If true, keep warnings as "system message" paragraphs in the built documents. 135 | #keep_warnings = False 136 | 137 | 138 | # -- Options for HTML output ---------------------------------------------- 139 | 140 | # The theme to use for HTML and HTML Help pages. See the documentation for 141 | # a list of builtin themes. 142 | import os 143 | on_rtd = os.environ.get('READTHEDOCS', None) == 'True' 144 | 145 | if not on_rtd: # only import and set the theme if we're building docs locally 146 | import sphinx_rtd_theme 147 | html_theme = 'sphinx_rtd_theme' 148 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 149 | 150 | # Theme options are theme-specific and customize the look and feel of a theme 151 | # further. For a list of options available for each theme, see the 152 | # documentation. 153 | #html_theme_options = {} 154 | 155 | # Add any paths that contain custom themes here, relative to this directory. 156 | html_theme_path = [] 157 | 158 | # The name for this set of Sphinx documents. If None, it defaults to 159 | # " v documentation". 160 | #html_title = None 161 | 162 | # A shorter title for the navigation bar. Default is the same as html_title. 163 | #html_short_title = None 164 | 165 | # The name of an image file (relative to this directory) to place at the top 166 | # of the sidebar. 167 | #html_logo = None 168 | 169 | # The name of an image file (within the static path) to use as favicon of the 170 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 171 | # pixels large. 172 | #html_favicon = None 173 | 174 | # Add any paths that contain custom static files (such as style sheets) here, 175 | # relative to this directory. They are copied after the builtin static files, 176 | # so a file named "default.css" will overwrite the builtin "default.css". 177 | html_static_path = ['_static'] 178 | 179 | # Add any extra paths that contain custom files (such as robots.txt or 180 | # .htaccess) here, relative to this directory. These files are copied 181 | # directly to the root of the documentation. 182 | #html_extra_path = [] 183 | 184 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 185 | # using the given strftime format. 186 | html_last_updated_fmt = '%b %d, %Y' 187 | 188 | # If true, SmartyPants will be used to convert quotes and dashes to 189 | # typographically correct entities. 190 | #html_use_smartypants = True 191 | 192 | # Custom sidebar templates, maps document names to template names. 193 | #html_sidebars = {} 194 | 195 | # Additional templates that should be rendered to pages, maps page names to 196 | # template names. 197 | #html_additional_pages = {} 198 | 199 | # If false, no module index is generated. 200 | html_domain_indices = True 201 | 202 | # If false, no index is generated. 203 | #html_use_index = True 204 | 205 | # If true, the index is split into individual pages for each letter. 206 | #html_split_index = False 207 | 208 | # If true, links to the reST sources are added to the pages. 209 | #html_show_sourcelink = True 210 | 211 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 212 | #html_show_sphinx = True 213 | 214 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 215 | #html_show_copyright = True 216 | 217 | # If true, an OpenSearch description file will be output, and all pages will 218 | # contain a tag referring to it. The value of this option must be the 219 | # base URL from which the finished HTML is served. 220 | #html_use_opensearch = '' 221 | 222 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 223 | #html_file_suffix = None 224 | 225 | # Output file base name for HTML help builder. 226 | htmlhelp_basename = 'pydvbcss' 227 | 228 | 229 | # -- Options for LaTeX output --------------------------------------------- 230 | 231 | latex_elements = { 232 | # The paper size ('letterpaper' or 'a4paper'). 233 | #'papersize': 'letterpaper', 234 | 235 | # The font size ('10pt', '11pt' or '12pt'). 236 | #'pointsize': '10pt', 237 | 238 | # Additional stuff for the LaTeX preamble. 239 | #'preamble': '', 240 | } 241 | 242 | # Grouping the document tree into LaTeX files. List of tuples 243 | # (source start file, target name, title, 244 | # author, documentclass [howto, manual, or own class]). 245 | latex_documents = [ 246 | ('index', 'pydvbcss.tex', u'"pydvbcss - a library implementing DVB protocols for Companion Screen Synchronisation', 247 | u'British Broadcasting Corporation', 'manual'), 248 | ] 249 | 250 | # The name of an image file (relative to this directory) to place at the top of 251 | # the title page. 252 | #latex_logo = None 253 | 254 | # For "manual" documents, if this is true, then toplevel headings are parts, 255 | # not chapters. 256 | #latex_use_parts = False 257 | 258 | # If true, show page references after internal links. 259 | #latex_show_pagerefs = False 260 | 261 | # If true, show URL addresses after external links. 262 | #latex_show_urls = False 263 | 264 | # Documents to append as an appendix to all manuals. 265 | #latex_appendices = [] 266 | 267 | # If false, no module index is generated. 268 | #latex_domain_indices = True 269 | 270 | 271 | # -- Options for manual page output --------------------------------------- 272 | 273 | # One entry per manual page. List of tuples 274 | # (source start file, name, description, authors, manual section). 275 | man_pages = [ 276 | ('index', 'pydvbcss', u'"pydvbcss - a library implementing DVB protocols for Companion Screen Synchronisation', 277 | [u'British Broadcasting Corporation'], 1) 278 | ] 279 | 280 | # If true, show URL addresses after external links. 281 | #man_show_urls = False 282 | 283 | 284 | # -- Options for Texinfo output ------------------------------------------- 285 | 286 | # Grouping the document tree into Texinfo files. List of tuples 287 | # (source start file, target name, title, author, 288 | # dir menu entry, description, category) 289 | texinfo_documents = [ 290 | ('index', 'pydvbcss', u'Python DVB CSS Library', 291 | u'British Broadcasting Corporation', 'pydvbcss', 292 | "pydvbcss - a library implementing DVB protocols for Companion Screen Synchronisation.", 293 | 'Miscellaneous'), 294 | ] 295 | 296 | # Documents to append as an appendix to all manuals. 297 | #texinfo_appendices = [] 298 | 299 | # If false, no module index is generated. 300 | #texinfo_domain_indices = True 301 | 302 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 303 | #texinfo_show_urls = 'footnote' 304 | 305 | # If true, do not generate a @detailmenu in the "Top" node's menu. 306 | #texinfo_no_detailmenu = False 307 | 308 | 309 | # Example configuration for intersphinx: refer to the Python standard library. 310 | intersphinx_mapping = { 311 | 'python' : ('http://docs.python.org/', None), 312 | 'ws4py' : ('https://ws4py.readthedocs.org/en/latest/', None), 313 | } 314 | 315 | -------------------------------------------------------------------------------- /docs/correlated-clock-speed-change-issue.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbc/pydvbcss/8725c0c736e374466b0adb239be17e937edc733c/docs/correlated-clock-speed-change-issue.graffle -------------------------------------------------------------------------------- /docs/correlated-clock-speed-change-issue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbc/pydvbcss/8725c0c736e374466b0adb239be17e937edc733c/docs/correlated-clock-speed-change-issue.png -------------------------------------------------------------------------------- /docs/correlated-clock.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbc/pydvbcss/8725c0c736e374466b0adb239be17e937edc733c/docs/correlated-clock.graffle -------------------------------------------------------------------------------- /docs/correlated-clock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbc/pydvbcss/8725c0c736e374466b0adb239be17e937edc733c/docs/correlated-clock.png -------------------------------------------------------------------------------- /docs/examples.rst: -------------------------------------------------------------------------------- 1 | .. _examples: 2 | example code 3 | 4 | .. py:module:: examples 5 | 6 | ================ 7 | Run the examples 8 | ================ 9 | 10 | The code in the `examples` directory demonstrates how to create and control 11 | servers and clients for all three protocols: CSS-CII, CSS-TS and CSS-WC. 12 | 13 | .. contents:: 14 | :local: 15 | :depth: 1 16 | 17 | There are instructions below on how to run the examples and see them interact with each other. 18 | 19 | See the sources here: :repo:`on github ` 20 | 21 | **WallClockServer.py** and **WallClockClient.py** 22 | ================================================= 23 | 24 | Get started 25 | ----------- 26 | 27 | The *WallClockServer* and *WallClockClient* examples use the library to 28 | implement a simple server and client for the :doc:`wc`. 29 | 30 | First start the server, specifying the host and IP to listen on: 31 | 32 | .. code-block:: shell 33 | 34 | $ python examples/WallClockServer.py 127.0.0.1 6677 35 | 36 | Leave it running in the background and start a client, telling it where to connect to the server: 37 | 38 | .. code-block:: shell 39 | 40 | $ python examples/WallClockClient.py 127.0.0.1 6677 41 | 42 | .. note:: 43 | 44 | The wall clock protocol is connectionless (it uses UDP) This means the client will not report an error if 45 | you enter the wrong IP address or port number. 46 | 47 | Watch the "dispersion" values which indicate how 48 | much margin for error there is in the client's wall clock estimate. If the value is very large, this 49 | means it is not receiving responses from the server. 50 | 51 | 52 | How they work 53 | ------------- 54 | 55 | WallClockServer.py :repo:`[source] ` 56 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 57 | 58 | .. automodule:: examples.WallClockServer 59 | :noindex: 60 | 61 | WallClockClient.py :repo:`[source] ` 62 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 63 | 64 | .. automodule:: examples.WallClockClient 65 | :noindex: 66 | 67 | 68 | 69 | 70 | **CIIServer.py** and **CIIClient.py** 71 | ===================================== 72 | 73 | Get started 74 | ----------- 75 | 76 | The `CIIServer` and `CIIClient` examples implement the *CSS-CII* 77 | protocol, with the server sharing some pretend CII status information with 78 | the client. 79 | 80 | First start the server: 81 | 82 | .. code-block:: shell 83 | 84 | $ python examples/CIIServer.py 85 | 86 | The server listens on 127.0.0.1 on port 7681 and accepts WebSocket connections to `ws://:/cii`. 87 | 88 | Leave it running in the background and connect using the client and see how 89 | the CII data is pushed by the server whenever it changes: 90 | 91 | .. code-block:: shell 92 | 93 | $ python examples/CIIClient.py ws:/127.0.0.1:7681/cii 94 | 95 | 96 | 97 | How they work 98 | ------------- 99 | 100 | CIIServer.py :repo:`[source] ` 101 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 102 | 103 | .. automodule:: examples.CIIServer 104 | :noindex: 105 | 106 | 107 | CIIServer.py :repo:`[source] ` 108 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 109 | 110 | .. automodule:: examples.CIIClient 111 | :noindex: 112 | 113 | 114 | 115 | 116 | **TSServer.py** and **TSClient.py** 117 | =================================== 118 | 119 | Get started 120 | ----------- 121 | 122 | The `TServer` and `TSClient` examples implement the *CSS-TS* protocol, with the 123 | server pretending to have a few different timelines for a DVB broadcast service (where the content ID is a DVB URL). 124 | 125 | First start the server: 126 | 127 | .. code-block:: shell 128 | 129 | $ python examples/TSServer.py 130 | 131 | The server listens on 127.0.0.1 on port 7681 and accepts WebSocket connections to `ws://:/ts`. 132 | It also includes a wall clock server, also on 127.0.0.1 on port 6677. 133 | 134 | Leave it running in the background and connect using the client and see 135 | how the client is able to synchronise and periodically print an estimate of the timeline position (converted to units of seconds): 136 | 137 | .. code-block:: shell 138 | 139 | $ python examples/TSClient.py ws://127.0.0.1:7681/ts udp://127.0.0.1:6677 "dvb://" "urn:dvb:css:timeline:pts" 90000 140 | 141 | Here we have told it to request a timeline for whatever content the server thinks it is showing provided that the content ID begins with "dvb://". 142 | Assuming that matches, then the timeline is to be a PTS timeline, which ticks at 90kHz (the standard rate of PTS in an MPEG transport stream). 143 | 144 | 145 | 146 | How they work 147 | ------------- 148 | 149 | TSServer.py :repo:`[source] ` 150 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 151 | 152 | .. automodule:: examples.TSServer 153 | :noindex: 154 | 155 | 156 | TSClient.py :repo:`[source] ` 157 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 158 | 159 | .. automodule:: examples.TSClient 160 | :noindex: 161 | 162 | 163 | 164 | **TVDevice.py** 165 | =============== 166 | 167 | Get started 168 | ----------- 169 | 170 | This is a very simple example of a server running all three protocols (CSS-WC, CSS-TS and CSS-CII). 171 | It pretends to be showing a DVB broadcast service and able to provide a PTS or TEMI timeline for it. 172 | 173 | First start the server: 174 | 175 | .. code-block:: shell 176 | 177 | $ python examples/TVDevice.py 178 | 179 | While we leave it running in the background, we can try to interact with it using the various example 180 | clients described above. 181 | 182 | By default it provides a wall clock server on all interfaces on port 6677 183 | 184 | .. code-block:: shell 185 | 186 | $ python examples/WallClockClient.py 127.0.0.1 6677 187 | 188 | ... and a CSS-CII server that can be reached at `ws://{{host}}:7681/cii` where `{{host}}` is any 189 | interface, including 127.0.0.1. 190 | 191 | .. code-block:: shell 192 | 193 | $ python examples/CIIClient.py ws:/127.0.0.1:7681/cii 194 | 195 | ... and a CSS-TS server that can be reached at `ws://{{host}}:7681/ts` 196 | 197 | .. code-block:: shell 198 | 199 | $ python examples/TSClient.py ws://127.0.0.1:7681/ts udp://127.0.0.1:6677 "dvb://" "urn:dvb:css:timeline:temi:1:1" 1000 200 | 201 | 202 | 203 | How it works 204 | ------------ 205 | 206 | TVDevice.py :repo:`[source] ` 207 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 208 | 209 | .. automodule:: examples.TVDevice 210 | :noindex: 211 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | :tocdepth: 2 2 | 3 | .. pydvbcss documentation master file 4 | 5 | pydvbcss 6 | ======== 7 | 8 | **DVB protocols for synchronisation between TV Devices and Companion Screen Applications.** 9 | 10 | :Release: |release| 11 | :Licence: `Apache License v2.0 `_. 12 | :Source: :repo:`/` 13 | :How to install: :repo:`/README.md` 14 | :Changelog: :repo:`/CHANGELOG.md` 15 | 16 | .. toctree:: 17 | :maxdepth: 1 18 | 19 | examples 20 | protocol 21 | timing 22 | internals 23 | 24 | * :ref:`modindex` | :ref:`Full Index ` 25 | 26 | This collection of Python modules provides clients and servers for the network protocols defined in the 27 | DVB "Companion Screens and Streams" (CSS) specification `ETSI 103 286 part 2 `_. 28 | There are also supporting classes that model clocks (e.g. to represent timelines) and their inter-relationships. 29 | 30 | Use it to build clients and servers for each of the protocols (CSS-WC, CSS-CII and CSS-TS) 31 | that mock or simulate the roles of TV Devices and Companion Screen Applications 32 | for testing and prototyping. 33 | 34 | *To use this library you need to have a working understanding of these protocols and, of course, the Python programming language.* 35 | 36 | 37 | .. _gettingstarted: 38 | 39 | Getting started 40 | =============== 41 | 42 | #. Install, following the instructions in the :repo:`README `. 43 | 44 | #. Try to :doc:`examples`. 45 | 46 | #. Read the docs for the :doc:`protocol`. 47 | 48 | #. Use the library in you own code 49 | 50 | State of implementation 51 | ======================= 52 | 53 | This library does not currently implement the `CSS-TE` or `CSS-MRS` protocols (from the DVB specification). 54 | 55 | There are some unit tests but these mainly only cover the calculations done within clock objects and the packing and unpacking of JSON messages. 56 | 57 | 58 | Upgrading from previous versions 59 | ================================ 60 | 61 | See the :repo:`release notes / change log ` for details of what is new in this version of pydvbcss. 62 | 63 | 64 | 65 | License and Contributing 66 | ======================== 67 | 68 | *pydvbcss* is licensed as open source software under the terms of the 69 | `Apache License v2.0 `_. 70 | 71 | See the `CONTRIBUTING` and `AUTHORS` files for information on how to contribute and who has contributed to this library. 72 | 73 | 74 | Contact and discuss 75 | =================== 76 | 77 | There is a `pydvbcss google group `_ for announcements and discussion of this library. 78 | -------------------------------------------------------------------------------- /docs/internals.rst: -------------------------------------------------------------------------------- 1 | =============================== 2 | Internal implementation details 3 | =============================== 4 | 5 | .. toctree:: 6 | :maxdepth: 1 7 | 8 | servers-internals.rst 9 | task-internals.rst 10 | 11 | Here are some details on parts of the internal implementation of aspects of this library. 12 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\PythonDVBSynccode.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\PythonDVBSynccode.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end 243 | -------------------------------------------------------------------------------- /docs/monotonic_time.rst: -------------------------------------------------------------------------------- 1 | .. py:module:: dvbcss.monotonic_time 2 | 3 | ======================= 4 | Montonic time functions 5 | ======================= 6 | 7 | Module: `dvbcss.monotonic_time` 8 | 9 | .. contents:: 10 | :local: 11 | :depth: 2 12 | 13 | .. automodule:: dvbcss.monotonic_time 14 | :noindex: 15 | 16 | 17 | Functions 18 | --------- 19 | 20 | time(), timeMicros() and timeNanos() 21 | '''''''''''''''''''''''''''''''''''' 22 | 23 | .. autofunction:: time 24 | 25 | .. autofunction:: timeMicros 26 | 27 | .. autofunction:: timeNanos 28 | 29 | 30 | sleep() 31 | ''''''' 32 | 33 | .. autofunction:: sleep 34 | -------------------------------------------------------------------------------- /docs/protocol.rst: -------------------------------------------------------------------------------- 1 | .. py:module:: dvbcss.protocol 2 | 3 | ======================== 4 | DVB CSS Protocol modules 5 | ======================== 6 | 7 | Module: `dvbcss.protocol` 8 | 9 | The :mod:`dvbcss.protocol` module contains classes to implement the CSS-CII, CSS-TS and CSS-WC protocols. 10 | For each protocol there are objects to represent the messages that flow across the protocols and classes 11 | that implement clients and servers for the protocols. 12 | 13 | .. toctree:: 14 | :maxdepth: 2 15 | 16 | cii.rst 17 | ts.rst 18 | wc.rst 19 | 20 | See :doc:`servers-internals` for information on how the servers are implemented. 21 | 22 | Common types and objects 23 | ------------------------ 24 | 25 | .. _private-data: 26 | 27 | Signalling that a property is to be omitted from a message 28 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 29 | 30 | .. autodata:: OMIT 31 | :annotation: object 32 | 33 | Private data 34 | ~~~~~~~~~~~~ 35 | 36 | Some protocol messages contain optional properties to carry private data. 37 | 38 | Private data is encoded in message objects here as a :class:`list` of :class:`dict` objects 39 | where each has a key "type" whose value is a URI. 40 | 41 | Each dict can contain any other 42 | keys and values you wish so long as they can be parsed by the python :mod:`json ` module's encoder. 43 | For example: 44 | 45 | .. code-block:: python 46 | 47 | example_private_data = [ 48 | { "type" : "urn:bbc.co.uk:pid", "pid":"b00291843", 49 | "entity":"episode" 50 | }, 51 | { "type" : "tag:bbc.co.uk/programmes/clips/link-url", 52 | "http://www.bbc.co.uk/programmes/b1290532/" 53 | } 54 | ] 55 | 56 | 57 | Exceptions 58 | ~~~~~~~~~~ 59 | 60 | .. autoclass:: dvbcss.protocol.client.ConnectionError 61 | -------------------------------------------------------------------------------- /docs/range-correlated-clock.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbc/pydvbcss/8725c0c736e374466b0adb239be17e937edc733c/docs/range-correlated-clock.graffle -------------------------------------------------------------------------------- /docs/range-correlated-clock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbc/pydvbcss/8725c0c736e374466b0adb239be17e937edc733c/docs/range-correlated-clock.png -------------------------------------------------------------------------------- /docs/servers-internals.rst: -------------------------------------------------------------------------------- 1 | .. _servers-internals: 2 | .. py:module:: dvbcss.protocol.server 3 | 4 | ====================================== 5 | Protocol server implementation details 6 | ====================================== 7 | 8 | .. contents:: 9 | :local: 10 | :depth: 2 11 | 12 | CSS-WC 13 | ------ 14 | 15 | Overview 16 | ^^^^^^^^ 17 | 18 | The CSS-WC server is based on a simple generic framework for building UDP servers 19 | 20 | Classes 21 | ^^^^^^^ 22 | 23 | .. autoclass:: dvbcss.protocol.server.wc.UdpRequestServer 24 | :members: 25 | :inherited-members: 26 | 27 | .. autoclass:: dvbcss.protocol.server.wc.WallClockServerHandler 28 | :members: 29 | :inherited-members: 30 | 31 | 32 | CSS-CII and CSS-TS 33 | ------------------ 34 | 35 | Module: `dvbcss.protocol.server` 36 | 37 | Overview 38 | ^^^^^^^^ 39 | 40 | The CSS-CII and CSS-TS servers subclass the WebSocket server functionality for cherrypy implemented 41 | by ws4py in the :mod:`~ws4py.server.cherrypyserver` module. 42 | 43 | :class:`~cii.CIIServer` and :class:`~ts.TSServer` both inherit from a common base implementation 44 | :class:`WSServerTool` provided in the `dvbcss.protocol.server` module. 45 | 46 | The Tool provides the hook into cherrypy for handling the connection request and upgrading it to a 47 | WebSocket connection, spawning an object representing the WebSocket connection and which implements 48 | the WebSocket protocols. 49 | 50 | The base server object class is intended to manage all WebSocket connections for a particular server 51 | endpoint. It therefore provides its own customised WebScoket class that is bound to that particular 52 | server object instance. 53 | 54 | The tool is enabled via an "on" configuration when setting up the mount point in cherrypy. 55 | The tool also expects to a "handler_cls" property set in the configuration at the mount point. 56 | This property points to a WebSocket class which can be instantiated to handle the connection. 57 | 58 | Example usage: creating a server at "ws://:80/endpoint" just using the base classes provided here: 59 | 60 | .. code-block:: python 61 | 62 | import cherrypy 63 | from ws4py.server.cherrypyserver import WebSocketPlugin 64 | from dvbcss.protocol.server import WSServerBase, WSServerTool 65 | 66 | # plug the tool into cherrypy as "my_server" 67 | cherrypy.tools.my_server = WSServerTool() 68 | 69 | WebSocketPlugin(cherrypy.engine).subscribe() 70 | 71 | # create my server 72 | myServer = WSServerBase() 73 | 74 | # bind it to the URL path /endpoint in the cherrypy server 75 | class Root(object): 76 | @cherrypy.expose 77 | def endpoint(self): 78 | pass 79 | 80 | cfg = {"/endpoint": {'tools.my_server.on': True, 81 | 'tools.my_server.handler_cls': myServer.handler 82 | } 83 | } 84 | 85 | cherrypy.tree.mount(Root(), "/", config=cfg) 86 | 87 | # activate cherrypy web server on port 80 88 | cherrypy.config.update({"server.socket_port":80}) 89 | cherrypy.engine.start() 90 | 91 | 92 | See documentation for :class:`WSServerBase` for information on creating subclasses to implement specific endpoints. 93 | 94 | 95 | Classes 96 | ^^^^^^^ 97 | 98 | .. autoclass:: dvbcss.protocol.server.ConnectionIdGen 99 | :members: 100 | :inherited-members: 101 | 102 | .. autoclass:: dvbcss.protocol.server.WSServerTool 103 | :members: 104 | :inherited-members: 105 | 106 | .. autoclass:: dvbcss.protocol.server.WSServerBase 107 | :members: 108 | :exclude-members: handler 109 | :inherited-members: 110 | :private-members: 111 | 112 | .. autodata:: loggingName, getDefaultConnectionData, connectionIdPrefix 113 | 114 | .. autoinstanceattribute:: handler 115 | :annotation: 116 | 117 | .. autoinstanceattribute:: _connections 118 | :annotation: 119 | 120 | .. :method: _makeHandlerClass 121 | 122 | .. class:: .WebSocketHandler(WebSocket) 123 | 124 | This class is created and returned by the :func:`WSServerBase._makeHandlerClass` method 125 | and each class returned is bound to the instance of :class:`WSServerBase` that created it. 126 | 127 | It is intended to be provided to cherrypy as the "handler_cls" configuration parameter for the WebSocket tool. 128 | It is instantiated for every connection made. 129 | 130 | These are subclasses of the ws4py :class:`~ws4py.websocket.WebSocket` class and represent an individual WebSocket connection. 131 | 132 | Instances of this class call through to :func:`WSServerBase._addConnection` and :func:`WSServerBase._removeConnection` 133 | and :func:`WSServerBase._receivedMessage` to inform the parent server of the WebSocket opening, closing and receiving messages. 134 | 135 | .. classmethod:: isEnabled(cls) 136 | 137 | :return: True if the server endpoint is enabled, otherwise False. 138 | 139 | .. classmethod:: canAllocateConnection(cls) 140 | 141 | :return: True only if the connection limit of the parent server has not yet been reached. Otherwise False. 142 | 143 | .. method:: id(self) 144 | 145 | :return: A human readable connection ID 146 | -------------------------------------------------------------------------------- /docs/task-internals.rst: -------------------------------------------------------------------------------- 1 | .. _task-internals: 2 | 3 | =========================================== 4 | How the dvbcss.task module works internally 5 | =========================================== 6 | 7 | 8 | Introduction 9 | ------------ 10 | 11 | 12 | The :mod:`dvbcss.task` module internally implements a task scheduler based around a single daemon thread with an internal priority queue. 13 | 14 | Sleep and callback methods cause a task objet to be queued. The scheduler picks up the queued task and adds it to the priority queue and 15 | binds to the Clock so that it is notified of adjustments to the clock. When a task is added to the queue, the clock is queried to calculate 16 | the true time at which the tick count is expected to be reached by calling :func:`dvbcss.clock.ClockBase.calcWhen` 17 | 18 | If a clock is adjusted the affected tasks are marked as deprecated (but remain in the priority queue) and new tasks are rescheduled with a 19 | recalculated time. 20 | 21 | When one of the clocks involved has speed 0, then it may not be possible to calculate the time at which the task is to be scheduled. This 22 | happens when :func:`dvbcss.clock.ClockBase.calcWhen` returns :ref:`nan`. The task will not be immediately added to the priority queue, 23 | however it will be added later once the clock speed returns to a non-zero value. This happens automatically as part of the rescheduling 24 | process when a clock is adjusted. 25 | 26 | 27 | Objects 28 | ------- 29 | 30 | .. autodata:: dvbcss.task.scheduler 31 | 32 | Running instance of the :class:`dvbcss.task._Scheduler` 33 | 34 | 35 | Classes 36 | --------- 37 | 38 | .. autoclass:: dvbcss.task._Scheduler 39 | :members: 40 | 41 | .. autoclass:: dvbcss.task._Task 42 | :members: 43 | -------------------------------------------------------------------------------- /docs/timing.rst: -------------------------------------------------------------------------------- 1 | =================================== 2 | Clocks, Time and Scheduling modules 3 | =================================== 4 | 5 | This library contains a range of tools for dealing with timing, clocks, and timelines 6 | and scheduling code to run at set times. 7 | 8 | Contents: 9 | 10 | .. toctree:: 11 | :maxdepth: 2 12 | 13 | monotonic_time.rst 14 | clock.rst 15 | Task.rst 16 | 17 | 18 | The :mod:`dvbcss.monotonic_time` module provides a :func:`~dvbcss.monotonic_time.time` 19 | and :func:`~dvbcss.monotonic_time.sleep` functions equivalent to those in the 20 | standard python library :mod:`time` module. However these are guaranteeed to 21 | be monotonic and use the highest precision time sourcecs available (depending 22 | on the host operating system). 23 | 24 | The :mod:`dvbcss.clock` module provides high level abstractions for representing clocks 25 | and timelines and the relationships between them. The 26 | :doc:`client and server implementations ` 27 | for the DVB-CSS protocols use these objects to represent clocks and timelines. 28 | 29 | The:mod:`Task` module provides sleep and task scheduling functions that work 30 | with :mod:`~dvbcss.clock` objects and allow code to be called when a clock 31 | reaches a particular tick value, even if that clock is adjusted in some way 32 | after the task is scheduled. 33 | -------------------------------------------------------------------------------- /docs/ts-client.rst: -------------------------------------------------------------------------------- 1 | .. py:module:: dvbcss.protocol.client.ts 2 | 3 | ============== 4 | CSS-TS Clients 5 | ============== 6 | 7 | Module: `dvbcss.protocol.client.ts` 8 | 9 | .. contents:: 10 | :local: 11 | :depth: 2 12 | 13 | .. automodule:: dvbcss.protocol.client.ts 14 | :noindex: 15 | 16 | Classes 17 | ------- 18 | 19 | **TSClientConnection** 20 | '''''''''''''''''''''' 21 | 22 | .. autoclass:: TSClientConnection 23 | :members: 24 | :inherited-members: 25 | 26 | 27 | 28 | **TSClientClockController** 29 | ''''''''''''''''''''''''''' 30 | 31 | .. autoclass:: TSClientClockController 32 | :members: 33 | :exclude-members: connected, latestCt, earliestClock, latestClock 34 | :inherited-members: 35 | 36 | .. autoinstanceattribute:: connected 37 | :annotation: 38 | 39 | .. autoinstanceattribute:: latestCt 40 | :annotation: 41 | 42 | .. autoinstanceattribute:: earliestClock 43 | :annotation: 44 | 45 | .. autoinstanceattribute:: latestClock 46 | :annotation: 47 | 48 | 49 | -------------------------------------------------------------------------------- /docs/ts-messages.rst: -------------------------------------------------------------------------------- 1 | .. py:module:: dvbcss.protocol.ts 2 | 3 | ======================= 4 | CSS-TS Message objects 5 | ======================= 6 | 7 | Module: `dvbcss.protocol.ts` 8 | 9 | .. contents:: 10 | :local: 11 | :depth: 2 12 | 13 | .. automodule:: dvbcss.protocol.ts 14 | :noindex: 15 | 16 | Classes 17 | ~~~~~~~ 18 | 19 | setup-data 20 | ---------- 21 | 22 | .. autoclass:: SetupData 23 | :members: 24 | :exclude-members: contentIdStem, timelineSelector, private 25 | :inherited-members: 26 | 27 | .. autoinstanceattribute:: contentIdStem 28 | :annotation: 29 | 30 | .. autoinstanceattribute:: timelineSelector 31 | :annotation: 32 | 33 | .. autoinstanceattribute:: private 34 | :annotation: = OMIT 35 | 36 | 37 | Control Timestamp 38 | ----------------- 39 | 40 | .. autoclass:: ControlTimestamp 41 | :members: 42 | :exclude-members: timestamp, timelineSpeedMultiplier 43 | :inherited-members: 44 | 45 | .. autoinstanceattribute:: timestamp 46 | :annotation: 47 | 48 | .. autoinstanceattribute:: timelineSpeedMultiplier 49 | :annotation: 50 | 51 | 52 | 53 | AptEptLpt (Actual, Earliest and Latest Presentation Timestamp) 54 | -------------------------------------------------------------- 55 | 56 | .. autoclass:: AptEptLpt 57 | :members: 58 | :exclude-members: actual, earliest, latest 59 | :inherited-members: 60 | 61 | .. autoinstanceattribute:: actual 62 | :annotation: 63 | 64 | .. autoinstanceattribute:: earliest 65 | :annotation: 66 | 67 | .. autoinstanceattribute:: latest 68 | :annotation: 69 | 70 | 71 | 72 | Timestamp 73 | --------- 74 | 75 | This object does not directly represent a message, but is instead used by :class:`ControlTimestamp` and :class:`AptEptLpt` 76 | as a representation of a correlation between a content time and a wall clock time. 77 | 78 | .. autoclass:: Timestamp 79 | :members: 80 | :exclude-members: contentTime, wallClockTime 81 | :inherited-members: 82 | 83 | .. autoinstanceattribute:: contentTime 84 | :annotation: 85 | 86 | .. autoinstanceattribute:: wallClockTime 87 | :annotation: 88 | -------------------------------------------------------------------------------- /docs/ts-overview.rst: -------------------------------------------------------------------------------- 1 | CSS-TS Protocol introduction 2 | ---------------------------- 3 | 4 | **Here is a quick introduction to the CSS-TS protocol. For full details, refer to the** DVB specification 5 | `ETSI 103 286 part 2 `_. 6 | 7 | The CSS-TS protocol is for *Timeline Synchronisation*. Via this protocol, 8 | the server (e.g. TV) pushes timestamps to the client (e.g. companion) to keep 9 | it up-to-date on the progress of a particular timeline. The timeline to use 10 | is requested by the client at the beginning of the interaction. 11 | 12 | A client can also report its own timing and what range of timings it can 13 | cope with. This allows the client to negotiate a mutually achievable 14 | timing with the server, although the server is under no obligation and can 15 | choose to ignore this information. 16 | 17 | It is a WebSockets based protocol and messages are in JSON format. 18 | 19 | .. contents:: 20 | :local: 21 | :depth: 1 22 | 23 | Sequence of interaction 24 | ~~~~~~~~~~~~~~~~~~~~~~~ 25 | 26 | The client is assumed to already know the WebSocket URL for the CSS-TS server 27 | (usually from the information received via the :doc:`CSS-CII protocol `). 28 | 29 | 1. The client connects to the CSS-TS server. Either this is refused via an HTTP status code response, 30 | or it is accepted. 31 | 32 | 2. The client then immediately sends an initial :class:`~dvbcss.protocol.ts.SetupData` message to request the 33 | timeline to synchronise with. 34 | 35 | 3. The server then starts sending back :class:`~dvbcss.protocol.ts.ControlTimestamp` messages that update the 36 | client as to the state of that timeline. This state says either that the timeline is currently 37 | unavailable, or that it is available, and here is how to calculate the timeline position from 38 | the wall clock position.The server sends as frequently or infrequently as it likes, but will at least send 39 | them if there is a meaningful change in the timeline. 40 | 41 | 4. The client can, optionally, send its own :class:`~dvbcss.protocol.ts.AptEptLpt` messages to inform the server 42 | of what it is doing, and the range of different timings it can achieve for its media (e.g. 43 | what is the earliest and latest timings it can achieve). However this is purely informative. 44 | A server is not obliged to do anything with this information. 45 | 46 | 47 | Determining timeline selection and availability 48 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 49 | 50 | The :class:`~dvbcss.protocol.ts.SetupData` message conveys to the CSS-TS server details of what timeline the client 51 | wants to synchronise to. 52 | 53 | The CSS-TS server determines, at any given moment, if a timeline is available by checking if: 54 | 55 | 1. the stem matches the current content identifier for what is being presented at the server 56 | (meaning that the stem matches the left hand most subset of the content id); 57 | 58 | 2. and the timeline selector identifies a timeline that exists for the content being presented at the server. 59 | 60 | While the above is true, the timeline is "available". While it is not true, it is "unavailable". 61 | The CSS-TS connection is kept open irrespective of timeline availability. The server indicates 62 | changes in availability via the :class:`~dvbcss.protocol.ts.ControlTimestamp` messages it sends. 63 | 64 | Example :class:`~dvbcss.protocol.ts.SetupData` message; requesting a PTS timeline for 65 | a particular DVB broadcast channel, but not being specific about which event (programme in the EPG): 66 | 67 | .. code-block:: json 68 | 69 | { 70 | "contentIdStem" : "dvb://233a.1004.1044", 71 | "timelineSelector" : "urn:dvb:css:timeline:pts" 72 | } 73 | 74 | 75 | What does a timestamp convey? 76 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 77 | 78 | It represents a relationship between Wall Clock time and the timeline of the content being presented by the TV Device. 79 | It is sometimes referred to as a *(point of) correlation* between the wall clock and the timeline. 80 | 81 | This relationship can be visualised as a line that maps from wall clock time (on one axis) to timeline time (on the other axis). 82 | The (content-time, wall-clock-time) correlation is a point on the line. The timelineSpeedMultiplier represents the slope. 83 | The tick rates of each timeline are the units (the scale of each axis). 84 | 85 | The CSS-TS server sends :class:`~dvbcss.protocol.ts.ControlTimestamp` messages to clients, and clients can, optionally, send back :class:`AptEptLpt` 86 | messages. 87 | 88 | A ControlTimestamp can also tell a client if a timeline is unavailable by having null values for the contentTime 89 | and timelineSpeedMultiplier properties. Non-null values mean the timeline is available. 90 | 91 | :class:`~dvbcss.protocol.ts.AptEptLpt` messages enables a client to inform a server of what time it is presenting at (the "actual" part of the timestamp) 92 | and also to indicate the earliest and latest times it could present. It is, in effect, three correlations bundled into one message, 93 | to represent each of these three aspects. 94 | Earliest and Latest correlations are allowed to have -infinity and +infinity for the wall clock time to indicate that the client 95 | has no limits on how early, or late, it can present. 96 | 97 | An example :class:`~dvbcss.protocol.ts.ControlTimestamp` indicating the timeline is unavailable: 98 | 99 | .. code-block:: json 100 | 101 | { 102 | "contentTime" : null, 103 | "wallClockTime" : "116012000000", 104 | "timelineSpeedMultiplier" : null 105 | } 106 | 107 | An example :class:`~dvbcss.protocol.ts.ControlTimestamp` providing a correlation 108 | for an available timeline: 109 | 110 | .. code-block:: json 111 | 112 | 113 | { 114 | "contentTime" : "834188", 115 | "wallClockTime" : "116012000000", 116 | "timelineSpeedMultiplier" : 1.0 117 | } 118 | 119 | An example of an :class:`~dvbcss.protocol.ts.AptEptLpt` message, indicating 120 | the current presentation timing being used by the client; a limit on how early 121 | it can present; but no limit on how long it can delay (buffer): 122 | 123 | .. code-block:: json 124 | 125 | { 126 | "actual" : { 127 | "contentTime" : "834190", 128 | "wallClockTime" : "115992000000" 129 | }, 130 | "earliest" : { 131 | "contentTime" : "834190", 132 | "wallClockTime" : "115984000000" 133 | }, 134 | "latest" : { 135 | "contentTime" : "834190", 136 | "wallClockTime" : "plusinfinity" 137 | } 138 | } 139 | 140 | 141 | -------------------------------------------------------------------------------- /docs/ts-server.rst: -------------------------------------------------------------------------------- 1 | .. py:module:: dvbcss.protocol.server.ts 2 | 3 | ============== 4 | CSS-TS Servers 5 | ============== 6 | 7 | Module: `dvbcss.protocol.server.ts` 8 | 9 | .. contents:: 10 | :local: 11 | :depth: 2 12 | 13 | .. automodule:: dvbcss.protocol.server.ts 14 | :noindex: 15 | 16 | Classes 17 | ------- 18 | 19 | TSServer 20 | '''''''' 21 | 22 | .. autoclass:: TSServer 23 | :members: 24 | :exclude-members: contentId 25 | :inherited-members: 26 | 27 | .. autoinstanceattribute:: contentId 28 | :annotation: 29 | 30 | .. autoinstanceattribute:: handler 31 | :annotation: 32 | 33 | Handler class for new connections. 34 | 35 | When mounting the CII server with cherrypy, include in the config dict a key 'tools.dvb_cii.handler_cls' with this handler class as the value. 36 | 37 | 38 | TimelineSource 39 | '''''''''''''' 40 | 41 | .. autoclass:: TimelineSource 42 | :members: 43 | :inherited-members: 44 | 45 | 46 | SimpleTimelineSource 47 | '''''''''''''''''''' 48 | 49 | .. autoclass:: SimpleTimelineSource 50 | :members: 51 | :inherited-members: 52 | 53 | 54 | 55 | SimpleClockTimelineSource 56 | ''''''''''''''''''''''''' 57 | 58 | .. autoclass:: SimpleClockTimelineSource 59 | :members: 60 | :inherited-members: 61 | 62 | 63 | 64 | Functions 65 | --------- 66 | 67 | ciMatchesStem 68 | ''''''''''''' 69 | 70 | .. autofunction:: ciMatchesStem 71 | 72 | isControlTimestampChanged 73 | ''''''''''''''''''''''''' 74 | 75 | .. autofunction:: isControlTimestampChanged 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /docs/ts.rst: -------------------------------------------------------------------------------- 1 | =============== 2 | CSS-TS protocol 3 | =============== 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | 8 | ts-overview.rst 9 | ts-messages.rst 10 | ts-client.rst 11 | ts-server.rst 12 | 13 | This package provides objects for representing messages exchanged via the DVB CSS-TS protocol and for implementing clients and servers. 14 | 15 | The TS protocol is a mechanism for a server to share timeline position and playback speed with a client. In effect it enables a client to synchronise its 16 | understanding of the progress of media presentation with that of a server, in terms of a particular timeline. 17 | 18 | The client initially sends a :class:`~dvbcss.protocol.ts.SetupData` message to specify what timeline it wants to synchronise in terms of. 19 | The server then periodically sends :class:`~dvbcss.protocol.ts.ControlTimestamp` messages to inform the client of the state of presentation timing. 20 | The client can also send :class:`AptEptLpt ` (Actual, Earliest and Latest Presentation Timestamp) messages to the server 21 | to inform it of its playback timing and range of playback timings it can achieve. 22 | 23 | The client implementation in this library can control a :class:`~dvbcss.clock.CorrelatedClock`, synchronising it to the timeline. 24 | The server implementation in this library uses :class:`~dvbcss.clock.CorrelatedClock` objects as its source of timelines that it 25 | is to share with clients. 26 | 27 | 28 | Modules for using the CSS-TS protocol: 29 | 30 | * :mod:`dvbcss.protocol.ts` : objects for representing and packing/unpacking the CSS-TS protocol messages. 31 | 32 | * :mod:`dvbcss.protocol.client.ts` : implementation of a client for a CSS-TS connection. 33 | 34 | * :mod:`dvbcss.protocol.server.ts` : implementation of a server for a CSS-TS connection. 35 | 36 | -------------------------------------------------------------------------------- /docs/wc-client-clock-model.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbc/pydvbcss/8725c0c736e374466b0adb239be17e937edc733c/docs/wc-client-clock-model.graffle -------------------------------------------------------------------------------- /docs/wc-client-clock-model.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbc/pydvbcss/8725c0c736e374466b0adb239be17e937edc733c/docs/wc-client-clock-model.png -------------------------------------------------------------------------------- /docs/wc-client.rst: -------------------------------------------------------------------------------- 1 | .. py:module:: dvbcss.protocol.client.wc 2 | .. py:module:: dvbcss.protocol.client.wc.algorithm 3 | 4 | ============== 5 | CSS-WC Clients 6 | ============== 7 | 8 | Modules: `dvbcss.protocol.client.wc` | `dvbcss.protocol.client.wc.algorithm` 9 | 10 | 11 | .. contents:: 12 | :local: 13 | :depth: 3 14 | 15 | 16 | .. automodule:: dvbcss.protocol.client.wc 17 | :noindex: 18 | 19 | .. _algorithms: 20 | 21 | Algorithms 22 | ---------- 23 | 24 | .. automodule:: dvbcss.protocol.client.wc.algorithm 25 | :noindex: 26 | 27 | .. automodule:: dvbcss.protocol.client.wc.algorithm._filterpredict 28 | :noindex: 29 | 30 | Classes 31 | ------- 32 | 33 | 34 | WallClockClient 35 | ~~~~~~~~~~~~~~~ 36 | 37 | .. autoclass:: dvbcss.protocol.client.wc.WallClockClient 38 | :members: 39 | :inherited-members: 40 | 41 | 42 | Dispersion algorithm 43 | ~~~~~~~~~~~~~~~~~~~~ 44 | 45 | .. autoclass:: dvbcss.protocol.client.wc.algorithm.LowestDispersionCandidate 46 | :members: 47 | :inherited-members: 48 | 49 | Most recent measurement algorithm 50 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 51 | 52 | .. autoclass:: dvbcss.protocol.client.wc.algorithm.MostRecent 53 | :members: 54 | :inherited-members: 55 | 56 | Filter and Prediction composable algorithms 57 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 58 | 59 | Filters 60 | ''''''' 61 | 62 | .. autoclass:: dvbcss.protocol.client.wc.algorithm._filterpredict.FilterRttThreshold 63 | :members: 64 | :inherited-members: 65 | 66 | .. autoclass:: dvbcss.protocol.client.wc.algorithm._filterpredict.FilterLowestDispersionCandidate 67 | :members: 68 | :inherited-members: 69 | 70 | Predictors 71 | '''''''''' 72 | 73 | .. autoclass:: dvbcss.protocol.client.wc.algorithm._filterpredict.PredictSimple 74 | :members: 75 | :inherited-members: 76 | 77 | Functions 78 | --------- 79 | 80 | Filter and Prediction algorithm creator 81 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 82 | 83 | .. autofunction:: dvbcss.protocol.client.wc.algorithm.FilterAndPredict 84 | 85 | Candidate quality calculator 86 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 87 | 88 | This function is used internally by the :class:`~dvbcss.protocol.client.wc.WallClockClient` class. 89 | 90 | .. autofunction:: dvbcss.protocol.client.wc.algorithm.calcQuality 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /docs/wc-messages.rst: -------------------------------------------------------------------------------- 1 | .. py:module:: dvbcss.protocol.wc 2 | 3 | ======================= 4 | CSS-WC Message objects 5 | ======================= 6 | 7 | Module: `dvbcss.protocol.wc` 8 | 9 | .. contents:: 10 | :local: 11 | :depth: 2 12 | 13 | .. automodule:: dvbcss.protocol.wc 14 | :noindex: 15 | 16 | Classes 17 | ~~~~~~~ 18 | 19 | WCMessage 20 | --------- 21 | 22 | .. autoclass:: WCMessage 23 | :members: 24 | :exclude-members: msgtype, precision, maxFreqError, originateNanos, receiveNanos, transmitNanos, originalOriginate 25 | :inherited-members: 26 | 27 | .. autoinstanceattribute:: msgtype 28 | :annotation: 29 | 30 | .. autoinstanceattribute:: precision 31 | :annotation: 32 | 33 | .. autoinstanceattribute:: maxFreqError 34 | :annotation: 35 | 36 | .. autoinstanceattribute:: originateNanos 37 | :annotation: 38 | 39 | .. autoinstanceattribute:: receiveNanos 40 | :annotation: 41 | 42 | .. autoinstanceattribute:: transmitNanos 43 | :annotation: 44 | 45 | .. autoinstanceattribute:: originalOriginate 46 | :annotation: 47 | 48 | 49 | 50 | Candidate 51 | --------- 52 | 53 | .. autoclass:: Candidate 54 | :members: 55 | :exclude-members: t1, t2, t3, t4, offset, rtt, isNanos, precision, maxFreqError, msg 56 | :inherited-members: 57 | 58 | .. autoinstanceattribute:: t1 59 | :annotation: = msg.originateNanos 60 | 61 | .. autoinstanceattribute:: t2 62 | :annotation: = msg.receiveNanos 63 | 64 | .. autoinstanceattribute:: t3 65 | :annotation: = msg.transmitNanos 66 | 67 | .. autoinstanceattribute:: t4 68 | :annotation: = nanosRx 69 | 70 | .. autoinstanceattribute:: offset 71 | :annotation: = ((t3+t2)-(t4+t1))/2 72 | 73 | .. autoinstanceattribute:: rtt 74 | :annotation: = (t4-t1)-(t3-t2) 75 | 76 | .. autoinstanceattribute:: isNanos 77 | :annotation: = True 78 | 79 | .. autoinstanceattribute:: precision 80 | :annotation: = msg.getPrecision() 81 | 82 | .. autoinstanceattribute:: maxFreqError 83 | :annotation: = msg.getMaxFreqError() 84 | 85 | .. autoinstanceattribute:: msg 86 | :annotation: = WCMessage 87 | -------------------------------------------------------------------------------- /docs/wc-overview.rst: -------------------------------------------------------------------------------- 1 | CSS-WC Protocol Introduction 2 | ---------------------------- 3 | 4 | **Here is a quick introduction to the CSS-WC protocol. For full details, refer to the** 5 | DVB specification `ETSI 103 286 part 2 `_. 6 | 7 | The CSS-WC protocol is for establishing *Wall clock synchronisation* - meaning 8 | that there is a common synchronised sense of time (a "wall clock") between the 9 | server (e.g. TV) and client (e.g. companion). This common wall clock is used 10 | in the CSS-TS protocol to make it immune to network delays. 11 | 12 | The client uses the information carried in the protocol to estimate the server 13 | wall clock and attempt to compensate for network latency. This is a 14 | connectionless UDP protocol similar to NTP's client-server mode of 15 | operation, but much simplified and not intended to set the system real-time 16 | clock. 17 | 18 | .. contents:: 19 | :local: 20 | :depth: 1 21 | 22 | Sequence of interaction 23 | ~~~~~~~~~~~~~~~~~~~~~~~ 24 | 25 | The client is assumed to already know the host and port number of the CSS-WC 26 | server (usually from the information received via the 27 | :doc:`CSS-CII protocol `). 28 | 29 | 1. The client sends a Wall Clock protocol "request" message to the server. 30 | 31 | 2. The server sends back a Wall Clock protocol "response" message to the client. 32 | 33 | 3. If the server is able to more accurately measure when it sent a message 34 | *after* it has done so, then it can optionally send a "follow-up response" 35 | with this information. 36 | 37 | The client repeats this process as often as it needs to. 38 | 39 | Synchronising the wall clock 40 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 41 | 42 | The client notes the time at which the request is sent and the response received, 43 | and by the server including the times at which it received the request and 44 | sent its response. Using this information the client can estimate the relationship 45 | between the time of its clock and that of the server. It can also calculate 46 | an error bound on this (known as dispersion): 47 | 48 | .. image:: wc-request-response.png 49 | :width: 128pt 50 | :align: center 51 | 52 | Relationship expressed as an estimated offset: 53 | * Offset between local clock and server wall clock is (( *t3* + *t2* ) - ( *t4* + *t1* )) / 2 54 | 55 | Relationship expressed as a correlation: 56 | * When local clock is ( *t1* + *t4* )/2 57 | * ... the server wall clock is estimated to be ( *t2* + *t3* )/2 58 | 59 | The `DVB specification (part 2) `_ 60 | contains an annex that goes into more detail on the 61 | theory of how to calculate dispersion and how a client can use this 62 | as part of a simple algorithm to align its wall clock. 63 | 64 | 65 | Message format 66 | ~~~~~~~~~~~~~~ 67 | 68 | Requests and responses both have the same fixed 32 byte binary message format. 69 | A :class:`~dvbcss.protocol.wc.WCMessage` carries the following fields: 70 | 71 | * Protocol **version** identifier 72 | * Message **type** (request / response / response-before-follow-up / follow-up) 73 | * The **precision** of the server's wall clock 74 | * The **maximum frequency error** of the server's wall clock 75 | * Timevalues (in NTP 64-bit time format, comprising a 32bit word carrying the 76 | number of nanoseconds and another 32bit word containing the number of seconds) 77 | 78 | * **Originate timevalue:** when the client sent the request. 79 | * **Receive timevalue:** when the server received the request. 80 | * **Transmit timevalue:** when the server sent the response. 81 | 82 | The *precision*, *max freq error*, *receive timevalue* and *transmit timevalue* fields only 83 | have meaning in a response from a server. Their values do not matter in requests. -------------------------------------------------------------------------------- /docs/wc-request-response.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbc/pydvbcss/8725c0c736e374466b0adb239be17e937edc733c/docs/wc-request-response.graffle -------------------------------------------------------------------------------- /docs/wc-request-response.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbc/pydvbcss/8725c0c736e374466b0adb239be17e937edc733c/docs/wc-request-response.png -------------------------------------------------------------------------------- /docs/wc-server.rst: -------------------------------------------------------------------------------- 1 | .. py:module:: dvbcss.protocol.server.wc 2 | 3 | ============== 4 | CSS-WC Servers 5 | ============== 6 | 7 | .. contents:: 8 | :local: 9 | :depth: 2 10 | 11 | Modules: `dvbcss.protocol.server.wc` 12 | 13 | .. contents:: 14 | :local: 15 | :depth: 3 16 | 17 | .. automodule:: dvbcss.protocol.server.wc 18 | :noindex: 19 | 20 | 21 | Classes 22 | ------- 23 | 24 | WallClockServer 25 | ~~~~~~~~~~~~~~~ 26 | 27 | .. autoclass:: dvbcss.protocol.server.wc.WallClockServer 28 | :members: 29 | :inherited-members: 30 | 31 | 32 | -------------------------------------------------------------------------------- /docs/wc.rst: -------------------------------------------------------------------------------- 1 | ================ 2 | CSS-WC protocol 3 | ================ 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | 8 | wc-overview.rst 9 | wc-messages.rst 10 | wc-client.rst 11 | wc-server.rst 12 | 13 | This package provides objects for representing messages exchanged via the DVB CSS-WC protocol and for implementing clients and servers. 14 | 15 | The WC protocol is a simple UDP request response-protocol that enables simple 16 | `clock synchronisation `_ algorithms to 17 | be used to establish a common *wall clock* between a server and client. 18 | 19 | There is a :class:`~dvbcss.protocol.server.wc.WallClockServer` class providing a self contained Wall Clock server. 20 | The :class:`~dvbcss.protocol.client.wc.WallClockClient` is designed to allow different algorithms to be plugged in for acting on 21 | the results of the request-response interaction to adjust a local :mod:`~dvbcss.clock` object to match the Wall Clock of the 22 | server. 23 | 24 | Modules for using the CSS-WC protocol: 25 | 26 | * :mod:`dvbcss.protocol.wc` : objects for representing and packing/unpacking the protocol messages. 27 | 28 | * :mod:`dvbcss.protocol.client.wc` : implementation of a client for a CSS-WC connection. 29 | 30 | * :mod:`dvbcss.protocol.client.wc.algorithm` : algorithms to be used with a CSS-WC client. 31 | 32 | * :mod:`dvbcss.protocol.server.wc` : implementation of a server for a CSS-WC connection. 33 | 34 | -------------------------------------------------------------------------------- /dvbcss/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin env python 2 | 3 | # -------------------------------------------------------------------------- 4 | # Copyright 2015 British Broadcasting Corporation 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # -------------------------------------------------------------------------- 18 | 19 | import types 20 | 21 | def _inheritDocs(parentClass, objType="class"): 22 | """\ 23 | Decorator that copies documentation strings from attributes of a parent class to a child 24 | class where the methods have been defined but no doc string written. 25 | 26 | Works for methods that have been defined (instance method objects) and 27 | for getter/setter properties. 28 | 29 | Also suffix inheritance information. 30 | """ 31 | 32 | def processDoc(docString): 33 | objPathName = parentClass.__module__ + "." + parentClass.__name__ 34 | inheritInfo = "\n\n(Documentation inherited from :%s:`~%s`)\n" % (objType, objPathName) 35 | docString = docString.replace("|stub-method|","") 36 | docString = docString + inheritInfo 37 | return docString 38 | 39 | 40 | def decorator(childClass): 41 | 42 | for attrName in dir(parentClass): 43 | parentProp = getattr(parentClass, attrName) 44 | childProp = getattr(childClass, attrName) 45 | 46 | # depending on the kind of attribute, the __doc__ string comes from different places 47 | if type(parentProp) == types.MethodType and childProp.__doc__ is None: 48 | try: 49 | childProp.im_func.__doc__ = processDoc(parentProp.im_func.__doc__) 50 | except: 51 | pass 52 | 53 | elif type(parentProp) == property and childProp.__doc__ is None: 54 | docStr = processDoc(parentProp.__doc__) 55 | newProperty = property(fget=childProp.fget, fset=childProp.fset, fdel=childProp.fdel, doc=docStr) 56 | setattr(childClass, attrName, newProperty) 57 | 58 | else: 59 | pass 60 | 61 | return childClass 62 | 63 | return decorator 64 | -------------------------------------------------------------------------------- /dvbcss/protocol/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2015 British Broadcasting Corporation 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | class _Entity(object): 18 | """\ 19 | """ 20 | def __init__(self, name, description = None, **kwargs): 21 | super(_Entity,self).__init__(**kwargs) 22 | if description == None: 23 | description = name 24 | self.__doc__ = description 25 | self.name=name 26 | def __str__(self): 27 | return self.name 28 | def __repr__(self): 29 | return self.name 30 | 31 | OMIT=_Entity("OMIT", """\ 32 | When this object is assigned to an attribute of a protocol message object this 33 | indicates that the corresponding property is not included in the JSON 34 | representation of that message (it is omitted). 35 | 36 | Here is an example. By default nearly all properties of a freshly created CII 37 | message object are 'OMIT': 38 | 39 | .. code-block:: python 40 | 41 | >>> from dvbcss.protocol.cii import CII 42 | >>> from dvbcss.protocol import OMIT 43 | 44 | >>> c=CII() 45 | >>> print repr(c.contentId) 46 | OMIT 47 | 48 | >>> c.wcUrl = "udp://192.168.1.1:6677" 49 | >>> print repr(c.wcUrl) 50 | 'udp://192.168.1.1:6677' 51 | 52 | >>> c.wcUrl = OMIT 53 | >>> print repr(c.wcUrl) 54 | OMIT 55 | """) 56 | 57 | 58 | 59 | 60 | 61 | __all__ = [ 62 | "OMIT", 63 | ] 64 | -------------------------------------------------------------------------------- /dvbcss/protocol/client/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2015 British Broadcasting Corporation 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import sys 18 | 19 | try: 20 | from ws4py.client.threadedclient import WebSocketClient 21 | from ws4py.exc import HandshakeError 22 | except ImportError: 23 | print >> sys.stderr, "Requires 'ws4py' library. Install using PIP." 24 | sys.exit(1) 25 | 26 | 27 | class ConnectionError(Exception): 28 | "Exception that is raised when it was not possible to open a connection to a server." 29 | pass 30 | 31 | 32 | class WrappedWebSocket(WebSocketClient): 33 | def __init__(self, url, wrapper): 34 | self._wrapper = wrapper 35 | super(WrappedWebSocket,self).__init__(url) 36 | 37 | def connect(self): 38 | try: 39 | return WebSocketClient.connect(self) 40 | except: 41 | raise ConnectionError() 42 | 43 | def opened(self): 44 | WebSocketClient.opened(self) 45 | self._wrapper._ws_on_open() 46 | 47 | def closed(self, code, reason=None): 48 | WebSocketClient.closed(self, code, reason=reason) 49 | self._wrapper._ws_on_close(code, reason) 50 | 51 | def received_message(self, message): 52 | WebSocketClient.received_message(self,message) 53 | self._wrapper._ws_on_message(message) 54 | 55 | __all__ = [ ] 56 | 57 | -------------------------------------------------------------------------------- /dvbcss/protocol/client/wc/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2015 British Broadcasting Corporation 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | """\ 18 | The :class:`~dvbcss.protocol.client.wc.WallClockClient` class provides a complete Wall Clock Client implementation. 19 | It is used in conjunction with an :ref:`algorithm ` for adjusting a :mod:`~dvbcss.clock` object 20 | so that it mirrors the server's Wall Clock. 21 | 22 | 23 | Using a Wall Clock Client 24 | ------------------------- 25 | 26 | Recommended simplest use is to instantiate a :class:`~dvbcss.protocol.client.wc.WallClockClient` and provide it with an 27 | instance of the :class:`~dvbcss.protocol.client.wc.algorithm.LowestDispersionCandidate` algorithm to control how it updates a clock. 28 | 29 | A simple example that connects to a wall clock server at 192.168.0.115 port 6677 and sends requests once per second: 30 | 31 | .. code-block:: python 32 | 33 | from dvbcss.clock import SysClock as SysClock 34 | from dvbcss.protocol.client.wc import WallClockClient 35 | from dvbcss.protocol.client.wc.algorithm import LowestDispersionCandidate 36 | 37 | sysClock=SysClock() 38 | wallClock=CorrelatedClock(sysClock,tickRate=1000000000) 39 | 40 | algorithm = LowestDispersionCandidate(wallClock,repeatSecs=1,timeoutSecs=0.5) 41 | 42 | bind = ("0.0.0.0", 6677) 43 | server = ("192.168.0.115", 6677) 44 | 45 | wc_client=WallClockClient(bind, server, wallClock, algorithm) 46 | wc_client.start() 47 | 48 | # wall clock client is now running in the background 49 | 50 | 51 | After the :func:`~WallClockClient.start` method is called, the WallClockClient 52 | runs in a separate thread and execution continues. 53 | 54 | :class:`WallClockClient` is used by providing it with an object implementing the clock 55 | synchronisation :ref:`algorithm `. The Wall Clock client handles the 56 | protocol interaction (sending the requests and parsing the responses) at the times 57 | specified by the algorithm and passes the results of each request-response measurement 58 | (a :class:`~dvbcss.protocol.wc.Candidate`) to the algorithm. The algorithm then 59 | adjusts the clock. 60 | 61 | .. image:: wc-client-clock-model.png 62 | :width: 384pt 63 | :align: center 64 | 65 | .. versionchanged:: 0.4 66 | 67 | The measurement process involves taking a reading from the local clock when 68 | the request is about to be sent, and when the response is received. This measurement 69 | is taken from the *parent* of the clock you provide. The candidate represents 70 | a possible relationship between that (parent) clock and the Wall Clock of the 71 | server given the results of the request-response measurement. The algorithm 72 | processes this and makes a decision as to the :class:`~dvbcss.clock.Correlation` 73 | that is to be used. 74 | 75 | Although the WallClockClient class does not require the tickrate of the 76 | Wall Clock to be 1 tick per nanosecond, it is recommended to set it as such. 77 | This is because the next step (using the CSS-TS protocol) assumes a Wall Clock 78 | ticking at this rate. 79 | 80 | """ 81 | 82 | import threading 83 | import socket 84 | import logging 85 | import dvbcss.monotonic_time as time 86 | 87 | from dvbcss.protocol.wc import WCMessage, Candidate 88 | 89 | import algorithm 90 | 91 | class UdpRequestResponseClient(object): 92 | """\ 93 | Engine for running a simple request-response system. 94 | 95 | After creating, call start() to kick off the thread. 96 | 97 | stop() will stop the thread the next time the handler asks to 98 | send a request. 99 | 100 | You provide a handler to implement the protocol specifics. 101 | It should be a python generator function. 102 | 103 | The handler should 'yield' the following tuple whenever it wants 104 | a request sent to a server: 105 | ((payload, (addr,port), timeoutSecs) 106 | or if you don't want to send a message but want to wait to receive 107 | another message: 108 | (None, timeoutSecs) 109 | 110 | payload = the bytes to send, as a string 111 | addr = the IP address to send to, as a string 112 | port = the port number to send to, as a number 113 | timeoutSecs = the number of seconds (or fractions of a second) to wait for a response 114 | before timing out. 115 | 116 | The yield statement will return a tuple: 117 | (None, None) ... if timeout happened, otherwise... 118 | (reply, (srcaddr, srcport)) 119 | 120 | reply = the bytes received, as a string 121 | srcaddr = the IP address it came from, as a string 122 | srcport = the port number it came from, as a number 123 | 124 | 125 | e.g. a simple handler that sends a request every second(ish), 126 | and waits up to half a second for a reply 127 | 128 | def handler(): 129 | timeout=0.5 130 | request="request packet" 131 | dest=("127.0.0.1",5678) 132 | while True: 133 | print "Sending request to ",dest 134 | (reply,src) = (yield (req,dest),timeout) 135 | if reply is None: 136 | print "Timeout" 137 | else: 138 | print "Reply received from ",src 139 | print "Will wait 1 second before trying again" 140 | time.sleep(1.0) 141 | """ 142 | def __init__(self, socket, handlerIterator, maxMsgSize, **kwargs): 143 | """\ 144 | :param socket: (udp socket) Bound socket ready to receive (and send) UDP packets 145 | :param handlerIterator: (generator) Handler for initiating and receiving results of request-response interactions 146 | :param maxMsgSize: (int) Maximum anticipated message size in bytes. 147 | """ 148 | super(UdpRequestResponseClient,self).__init__(**kwargs) 149 | self.log=logging.getLogger("dvbcss.protocol.client.wc.UdpRequestResponseClient") 150 | self.socket = socket 151 | self.handler=handlerIterator 152 | self.maxMsgSize=maxMsgSize 153 | self.thread=None 154 | 155 | def start(self): 156 | """\ 157 | Call this method to start the client running. This function call returns immediately 158 | and the client proceeds to run in a thread in the background. 159 | 160 | Does nothing if the client is already running. 161 | """ 162 | if self.thread is not None: 163 | return 164 | self._pleaseStop=False 165 | self.thread = threading.Thread(target=self.run) 166 | self.thread.daemon=True 167 | self.log.debug("Starting") 168 | self.thread.start() 169 | 170 | def stop(self): 171 | """\ 172 | Call this method to stop the client running. Returns once the client has stopped. 173 | 174 | If the client is not running then nothing happens and this call returns immediately. 175 | """ 176 | if self.thread is None: 177 | return 178 | self._pleaseStop=True 179 | self.log.debug("Stopping") 180 | self.thread.join() 181 | self.log.debug("Stopped") 182 | 183 | def run(self): 184 | try: 185 | sendRequest, waitTimeSecs = self.handler.next() 186 | while not self._pleaseStop: 187 | if sendRequest is not None: 188 | sendPayload,sendDest = sendRequest 189 | self.socket.sendto(sendPayload,sendDest) 190 | self.socket.settimeout(waitTimeSecs) 191 | try: 192 | reply,src=self.socket.recvfrom(self.maxMsgSize) 193 | sendRequest, waitTimeSecs = self.handler.send((reply,src)) 194 | except socket.timeout: 195 | sendRequest, waitTimeSecs = self.handler.send((None,None)) 196 | except StopIteration: 197 | pass 198 | 199 | 200 | def _createUdpSocket((bindaddr, bindport)): 201 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 202 | s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 203 | s.settimeout(1.0) 204 | s.bind((bindaddr,bindport)) 205 | return s 206 | 207 | 208 | class WallClockClient(UdpRequestResponseClient): 209 | """\ 210 | Simple object implementing the client side of the CSS-TS protocol. 211 | 212 | Use by initialising and supplying with an algorithm object and Clock object. 213 | 214 | The :class:`~dvbcss.protocol.wc.Candidate` objects provided to the algorithm 215 | represent the relationship between the parent of the provided clock and the 216 | server's Wall Clock. 217 | 218 | The algorithm is then expected to set the :class:`dvbcss.clock.Correlation` 219 | of the clock object such that it becomes an estimate of the server's 220 | Wall Clock. 221 | 222 | It is recommended to use the :class:`~dvbcss.protocol.client.wc.algorithm.LowestDispersionCandidate` algorithm. 223 | """ 224 | def __init__(self, (bindaddr,bindport), (dstaddr,dstport), wallClock, wcAlgorithm): 225 | """\ 226 | **Initialisation takes the following parameters:** 227 | 228 | :param (bindaddr,bindport): (:class:`str`, :class:`int`) A tuple containing the IP address (as a string) and port (as an int) to bind to to listen for incoming packets 229 | :param (dstaddr,dstport): (:class:`str`, :class:`int`) A tuple containing the IP address (as a string) and port (as an int) of the Wall Clock server 230 | :param wallClock: (:mod:`~dvbcss.clock`) The local clock that will be controlled to be a Wall Clock. Measurements will be taken from its parent and candidates provided to the algorithm will represent the relationship between that (parent) clock and the server's wall clock. 231 | :param wcAlgorithm: (:ref:`algorithm `) The algorithm for the client to use to update the clock. 232 | 233 | .. versionchanged: 0.4 234 | 235 | The `clock` provided should be a :class:`~dvbcss.clock.CorrelatedClock`, Although 236 | :class:`~dvbcss.clock.TunableClock` should still work becuase it is a subclass of 237 | :class:`~dvbcss.clock.CorrelatedClock`. 238 | """ 239 | self.algorithm = wcAlgorithm #: (read only) The :ref:`algorithm ` object being used with this WallClockClient 240 | algGenerator = self.algorithm.algorithm() 241 | socket=_createUdpSocket((bindaddr,bindport)) 242 | msgSize=WCMessage.MSG_SIZE 243 | handler = algorithm.algorithmWrapper((dstaddr,dstport), wallClock.getParent(), algGenerator) 244 | super(WallClockClient, self).__init__(socket, handler, msgSize) 245 | 246 | 247 | 248 | __all__ = [ "WallClockClient", "algorithm" ] 249 | -------------------------------------------------------------------------------- /dvbcss/protocol/client/wc/algorithm/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2015 British Broadcasting Corporation 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | """\ 18 | 19 | The algorithm object determines when (and how often) a WallClockClient sends CSS-WC protocol requests 20 | and processes the results of requests and responses to adjust the :mod:`~dvbcss.clock` object representing 21 | the Wall Clock. 22 | 23 | 24 | Dispersion algorithm 25 | ~~~~~~~~~~~~~~~~~~~~ 26 | 27 | The :class:`~dvbcss.protocol.client.wc.algorithm.LowestDispersionCandidate` algorithm is the recommended algorithm. 28 | It uses the candidate with the lowest dispersion. 29 | 30 | 31 | Simple algorithm 32 | ~~~~~~~~~~~~~~~~ 33 | 34 | The :class:`~dvbcss.protocol.client.wc.algorithm.MostRecent` algorithm is a simple naive algorithm that uses 35 | the most recent candidate irrespective of its quality (e.g. how long the round-trip time of the measurement was) 36 | 37 | 38 | Writing new algorithms 39 | ~~~~~~~~~~~~~~~~~~~~~~ 40 | 41 | An algorithm is an object that has the following method: 42 | 43 | .. method:: .algorithm(self) 44 | 45 | A `python generator `_ 46 | function that yields whenever it wants a Wall Clock protocol measurement to be taken: 47 | 48 | .. code-block:: python 49 | 50 | candidateOrNone = yield timeoutSecs 51 | 52 | The yield must pass a timeout in seconds. This is the maximum amount of time the Wall Clock client will 53 | wait for a response to its request before timing out. 54 | 55 | .. versionchanged:: 0.4 56 | 57 | The yield statement will return either :class:`None` or a :class:`~dvbcss.protocol.wc.Candidate` object representing 58 | the result of the measurement. 59 | 60 | The algorithm can then use the Candidate object in its algorithm for estimating the wall clock (and in 61 | the case of most practical implementations: adjusting a :mod:`~dvbcss.clock` object). 62 | 63 | 64 | Here is an example of a simple naive algorithm that adjusts a :class:`~dvbcss.clock.CorrelatedClock` object 65 | using the most recent measurement, irrespective of influencing factors such as previous measurements or 66 | network latency (round trip time). It makes requests at most once per second and has a timeout on waiting 67 | for responses of 0.5 seconds: 68 | 69 | .. code-block:: python 70 | 71 | class NaiveAlgorithm(object): 72 | 73 | def __init__(self,clock): 74 | self.clock = clock 75 | 76 | def algorithm(self): 77 | while True: 78 | candidate=(yield 0.5) 79 | if candidate is not None: 80 | self.clock.correlation = candidate.calcCorrelationFor(self.clock) 81 | time.sleep(1.0) 82 | 83 | 84 | """ 85 | 86 | from dvbcss.protocol.wc import WCMessage, Candidate 87 | 88 | from dvbcss.protocol.client.wc.algorithm._dispersion import LowestDispersionCandidate 89 | from dvbcss.protocol.client.wc.algorithm._simple import MostRecent 90 | from dvbcss.protocol.client.wc.algorithm._filterpredict import FilterAndPredict, PredictSimple, FilterRttThreshold, FilterLowestDispersionCandidate 91 | 92 | import dvbcss.monotonic_time as time 93 | 94 | 95 | 96 | def algorithmWrapper(dest,measureClock,algorithm): 97 | """\ 98 | UdpRequestResponseClient handler function that wraps up the act of sending and receiving a WallClockMessage. 99 | Also handles the optional "follow-up" response type of message. If a response is received that indicates 100 | a follow-up is due, it will also wait for the follow-up. However the total time it will wait since the 101 | original request will not exceed the specified timeout. If only a response-promising-follow-up is received 102 | then that is what shall be returned. 103 | 104 | In turn, you plug an algorithm into it that is also a generator function. However that algorithm 105 | need only yield the timeout, and the return value will be a :class:`~dvbcss.protocol.wc.Candidate` 106 | object in units of nanoseconds. If timeout occurs the return value is None instead of a dict. 107 | 108 | :param dest: ("",port) The destination address of the server (to which the requests should be sent) 109 | :param measureClock: The :mod:`~dvbcss:Clock` from which the readings are taken (being `t1` and `t4` in the resulting candidate) 110 | :param algorithm: A generator that yields to request a WallClock request be sent, and acts on the responses. 111 | 112 | The generator function you provide, should use `yield` as follows: 113 | * to pass the timeout for waiting for a response as the yield value 114 | * to receive a :class:`~dvbcss.protocol.wc.Candidate` object representing the response. 115 | 116 | Example algorithm: 117 | 118 | .. code-block:: python 119 | 120 | def algorithm(): 121 | timeoutSecs=0.2 122 | while True: 123 | candidate=(yield timeoutSecs) 124 | if candidate is not None: 125 | print "Candidate received! ",candidate 126 | else: 127 | print "Timeout" 128 | print "Now waiting 1 second" 129 | time.sleep(1) 130 | 131 | sysClock = SysClock() 132 | wallClock = CorrelatedClock(sysClock, tickRate=1000000000) 133 | 134 | algWrapper = algorithmWrapper(destIpPort, sysClock, wallClock, algorithm()) 135 | 136 | """ 137 | try: 138 | # hand control to the algorithm. when it wants a request sent, it will 139 | # return and supply the timeout for waiting for a response 140 | timeoutSecs = algorithm.next() 141 | while True: 142 | # assemble a request 143 | reqMsg=WCMessage(WCMessage.TYPE_REQUEST, 0, 0, measureClock.nanos, 0, 0) 144 | toSend=reqMsg.pack(), dest 145 | 146 | # we'll send the request then seek the best quality response 147 | # until timeout, or terminating early if we get a quality > 2 148 | # response (meaning that it was a non-followed-up response or a 149 | # follow-up response). A lower quality response is one where 150 | # a follow-up is expected or if it related to a previous request 151 | 152 | # this design means it will pick the best response, but in the 153 | # absence of a reasonable one, it will still make do with whatever 154 | # it can get (e.g. a response relating to an earlier request) 155 | 156 | responseQuality = -999 157 | responseMsg = None 158 | responseRecvNanos = None 159 | 160 | timeoutBy = time.time() + timeoutSecs 161 | remainingTime = timeoutBy - time.time() 162 | 163 | while responseQuality < 3 and remainingTime > 0: 164 | # wait for a response. if first time round, send the request too 165 | latestResponse,src = (yield toSend,remainingTime) 166 | toSend=None 167 | 168 | # note when response was received 169 | latestResponseNanos=measureClock.nanos 170 | 171 | 172 | # did we get a response? did it come from the server we sent 173 | # the request to? 174 | if latestResponse is not None and src == dest: 175 | 176 | # assess the response and work out if better than any previous 177 | # response we're received 178 | latestResponseMsg = WCMessage.unpack(latestResponse) 179 | newQuality=calcQuality(reqMsg, latestResponseMsg) 180 | if newQuality >= responseQuality: 181 | responseQuality=newQuality 182 | responseMsg=latestResponseMsg 183 | responseRecvNanos=latestResponseNanos 184 | 185 | # work out how long left until timeout 186 | remainingTime=timeoutBy - time.time() 187 | 188 | 189 | if responseMsg is not None: 190 | candidate=Candidate(responseMsg,responseRecvNanos) 191 | else: 192 | # no message, no candidate 193 | candidate=None 194 | # pass the result to the algorithm, and wait for it to return when it 195 | # next wants a request to be sent (again supplying the response timeout) 196 | timeoutSecs=algorithm.send(candidate) 197 | except StopIteration: 198 | pass 199 | 200 | 201 | def calcQuality(reqMsg,respMsg): 202 | """\ 203 | Generate measure of how good the response was. Quality < 0 means response 204 | corresponded to a different request. 205 | 206 | Quality = 3 or 4 means it was a response for which no follow-up is expected 207 | or a follow-up response. 208 | 209 | Quality = 2 means it was a response for which a follow-up is expected 210 | """ 211 | if reqMsg.originateNanos == respMsg.originateNanos: 212 | # response corresponds to the request 213 | offset = 0 214 | else: 215 | # penalise because response corresponds to a different (presumably older) 216 | # request 217 | offset = -10 218 | 219 | if respMsg.msgtype == WCMessage.TYPE_RESPONSE: 220 | return offset+3 221 | elif respMsg.msgtype == WCMessage.TYPE_RESPONSE_WITH_FOLLOWUP: 222 | return offset+2 223 | elif respMsg.msgtype == WCMessage.TYPE_FOLLOWUP: 224 | return offset+4 225 | 226 | 227 | 228 | __all__ = [ 229 | "algorithmWrapper", 230 | "LowestDispersionCandidate", 231 | "MostRecent", 232 | "PredictSimple", 233 | "FilterRttThreshold", 234 | "FilterAndPredict", 235 | "FilterLowestDispersionCandidate", 236 | ] 237 | -------------------------------------------------------------------------------- /dvbcss/protocol/client/wc/algorithm/_dispersion.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2015 British Broadcasting Corporation 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | import dvbcss.monotonic_time as time 19 | import logging 20 | 21 | from dvbcss.clock import Correlation, CorrelatedClock 22 | import warnings 23 | 24 | class LowestDispersionCandidate(object): 25 | """\ 26 | Algorithm that selects the candidate (request-response measurement result) with the lowest 27 | dispersion. 28 | 29 | Dispersion is a formal measure of the error bounds of the Wall Clock estimate. This value is the sum 30 | of possible error due to: 31 | 32 | * measurement precision limitations (at both client and server) 33 | * round-trip time 34 | * maximum potential for oscillator frequency error at the client and at the server server 35 | This grows as the candidate (that was used to most recently adjust the clock) ages. 36 | 37 | .. note:: The Clock object must be the same one that is provided to the WallClockClient, otherwise 38 | this algorithm will not synchronise correctly. 39 | 40 | The tick rate of the Clock can be any tick rate (it does not have to be one tick per nanosecond), 41 | but too low a tick rate will limit clock synchronisation precision. 42 | 43 | There is a stub callback function provided that you can override (e.g. by subclassing) 44 | that will be called whenever the clock is adjusted: 45 | 46 | * :func:`onClockAdjusted` 47 | 48 | The arguments to this method provide details of when 49 | the adjustment took place and what adjustment was made. It also reports the dispersion 50 | before and after the adjustment and gives information needed to extrapolate future 51 | dispersions. You can use this, for example, to record the clock dispersion over time. 52 | """ 53 | def __init__(self,clock,repeatSecs=1.0,timeoutSecs=0.2,localMaxFreqErrorPpm=None): 54 | """\ 55 | *Initialisation takes the following parameters:* 56 | 57 | :param clock: A :class:`~dvbcss.clock.Correlated` object representing that will be adjusted to match the Wall Clock. 58 | :param repeatSecs: (:class:`float`) The rate at which Wall Clock protocol requests are to be sent (in seconds). 59 | :param timeoutSecs: (:class:`float`) The timeout on waiting for responses to requests (in seconds). 60 | :param localMaxFreqErrorPpm: Optional. Override using the :func:`~dvbcss.clock.ClockBase.getRootMaxFreqError` of the clock as the max freq error of the local clock, and instead use this value. It is the clock maximum frequency error in parts-per-million 61 | """ 62 | super(LowestDispersionCandidate,self).__init__() 63 | self.log=logging.getLogger("dvbcss.protocol.client.wc.algorithm.BestCandidateByDispersion") 64 | self.clock = clock 65 | self.repeatSecs = repeatSecs 66 | self.timeoutSecs = timeoutSecs 67 | self.localMaxFreqErrorPpm = localMaxFreqErrorPpm 68 | 69 | # force clock to register infinite dispersion initially 70 | self.clock.correlation = self.clock.correlation.butWith(initialError = float("+inf")) 71 | self.worstDispersion = float("+inf") 72 | 73 | def onClockAdjusted(self, timeAfterAdjustment, adjustment, oldDispersionNanos, newDispersionNanos, dispersionGrowthRate): 74 | """\ 75 | This method is called immediately after a clock adjustment has been made, 76 | and gives details on how the clock was changed and the effect on the dispersion. 77 | 78 | :param timeAfterAdjustment: The wall clock time (in ticks) immedaitely after the adjustment took place 79 | :param adjustment: The amount by which the wall clock instaneously changed (in ticks) 80 | :param oldDispersionNanos: The dispersion (in nanoseconds) just prior to adjustment 81 | :param newDispersionNanos: The dispersion (in nanoseconds) immediately after adjustment 82 | :param dispersionGrowthRate: The rate at which dispersion will continue to grow. 83 | 84 | To calculate a future dispersion at time T: 85 | futureDispersion = newDispersionNanos + (t-timeOfAdjustment) * dispersionGrowthRate 86 | 87 | |stub-method| 88 | """ 89 | pass 90 | 91 | def getCurrentDispersion(self): 92 | """\ 93 | :returns: Current dispersion at this moment in time in units of nanoseconds. 94 | """ 95 | return self.clock.dispersionAtTime(self.clock.ticks)*1000000000 96 | 97 | def algorithm(self): 98 | candidateClock = CorrelatedClock(self.clock.getParent(), tickRate=self.clock.tickRate, correlation=self.clock.correlation) 99 | 100 | while True: 101 | update = False 102 | cumulativeOffset = None 103 | candidate=(yield self.timeoutSecs) 104 | 105 | t = self.clock.ticks 106 | currentDispersion = self.clock.dispersionAtTime(t) 107 | 108 | if candidate is not None: 109 | 110 | candidateClock.correlation = candidate.calcCorrelationFor(self.clock, self.localMaxFreqErrorPpm) 111 | candidateDispersion = candidateClock.dispersionAtTime(t) 112 | 113 | update = candidateDispersion < currentDispersion 114 | if update: 115 | pt = self.clock.toParentTicks(t) 116 | adjustment = candidateClock.fromParentTicks(pt) - t 117 | if cumulativeOffset is None: 118 | cumulativeOffset=0 119 | else: 120 | cumulativeOffset+=adjustment 121 | 122 | self.clock.correlation = candidateClock.correlation 123 | self.onClockAdjusted(self.clock.ticks, adjustment, 1000000000*currentDispersion, 1000000000*candidateDispersion, self.clock.correlation.errorGrowthRate) 124 | 125 | else: 126 | pass 127 | 128 | # update worst dispersion seen so far 129 | self.worstDispersion = max(self.worstDispersion, currentDispersion, candidateDispersion) 130 | 131 | co = cumulativeOffset or 0 # convert None to 0 132 | self.log.info("Old / New dispersion (millis) is %.5f / %.5f ... offset=%20d new best candidate? %s\n" % (1000*currentDispersion, 1000*candidateDispersion, co, str(update))) 133 | else: 134 | self.log.info("Timeout. Dispersion (millis) is %.5f\n" % (1000*currentDispersion,)) 135 | # retry more quickly if we didn't get an improved candidate 136 | if update: 137 | time.sleep(self.repeatSecs) 138 | else: 139 | time.sleep(self.timeoutSecs) 140 | 141 | 142 | def getWorstDispersion(self): 143 | """\ 144 | Returns the worst (greatest) dispersion encountered since 145 | the previous time this method was called. 146 | 147 | The first time it is called, the value reported will be very large, 148 | reflecting the fact that initially dispersion is high because the 149 | client is not yet synchronised. 150 | 151 | :returns: dispersion 152 | """ 153 | dispersionNow = self.getCurrentDispersion() 154 | 155 | answer = max(self.worstDispersion, dispersionNow) 156 | 157 | # reset the worst dispersion, so it is measured from now until 158 | # next time this method is called 159 | self.worstDispersion = dispersionNow 160 | 161 | return answer 162 | 163 | 164 | 165 | class DispersionCalculator(object): 166 | """\ 167 | This is a legacy class from v0.3 and earlier and will be deprecated in the future. 168 | """ 169 | def __init__(self, clock, localPrecisionSecs, localMaxFreqErrorPpm): 170 | super(DispersionCalculator,self).__init__() 171 | warnings.warn("DispersionCalculator class is deprecated. Use Candidate.calcCorrelationFor() and Correlation.dispersionAtTime() instead.", DeprecationWarning) 172 | self.clock = clock 173 | self.precision = localPrecisionSecs 174 | self.maxFreqError = localMaxFreqErrorPpm 175 | 176 | def calc(self, candidate): 177 | return 1000000000*(candidate.precision + self.precision) \ 178 | + ( candidate.maxFreqError*(candidate.t3-candidate.t2) \ 179 | + self.maxFreqError*(candidate.t4-candidate.t1) 180 | + (candidate.maxFreqError+self.maxFreqError)*(self.clock.nanos - candidate.t4) \ 181 | ) / 1000000 + \ 182 | candidate.rtt/2 183 | 184 | def getGrowthRate(self, candidate): 185 | return (candidate.maxFreqError+self.maxFreqError) / 1000000.0 186 | -------------------------------------------------------------------------------- /dvbcss/protocol/client/wc/algorithm/_simple.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2015 British Broadcasting Corporation 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import logging 18 | import dvbcss.monotonic_time as time 19 | 20 | class MostRecent(object): 21 | """\ 22 | Simple (naive) Wall Clock client algorithm. 23 | 24 | Crash locks the supplied clock based on the offset observed in the 25 | result of every successful request/response. 26 | 27 | .. note:: The Clock object must be the same one that is provided to the WallClockClient, otherwise 28 | this algorithm will not synchronise correctly. 29 | 30 | The tick rate of the Clock can be any tick rate (it does not have to be one tick per nanosecond), 31 | but too low a tick rate will limit clock synchronisation precision. 32 | 33 | """ 34 | def __init__(self,clock,repeatSecs=1.0,timeoutSecs=0.2): 35 | """\ 36 | *Initialisation takes the following parameters:* 37 | 38 | :param clock: A :class:`~dvbcss.clock.CorrelatedClock` object representing that will be adjusted to match the Wall Clock. 39 | :param repeatSecs: (:class:`float`) The rate at which Wall Clock protocol requests are to be sent (in seconds). 40 | :param timeoutSecs: (:class:`float`) The timeout on waiting for responses to requests (in seconds). 41 | """ 42 | self.log=logging.getLogger("dvbcss.protocol.client.wc.algorithm.MostRecent") 43 | self.clock = clock 44 | self.repeatSecs = repeatSecs 45 | self.timeoutSecs = timeoutSecs 46 | 47 | def algorithm(self): 48 | while True: 49 | candidate=(yield self.timeoutSecs) 50 | if candidate is not None: 51 | self.log.info("Candidate: "+str(candidate)+"\n") 52 | self.clock.correlation = candidate.calcCorrelationFor(self.clock, 500) # guess as to local max freq error 53 | time.sleep(self.repeatSecs) 54 | else: 55 | self.log.debug("Response timeout\n") 56 | 57 | def getCurrentDispersion(self): 58 | """\ 59 | Calling this method will always result in `ValueError` being raised. 60 | 61 | :raises ValueError: This algorithm does not model clock synchronisation accuracy. 62 | """ 63 | raise ValueError("Unknown") 64 | -------------------------------------------------------------------------------- /dvbcss/protocol/transformers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2015 British Broadcasting Corporation 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | """\ 18 | Helper routines for protocol message format packing and unpacking code. 19 | 20 | Transformer objects provided here convert betwen python data types convenient for use within code and 21 | python types that will map directly to JSON types. Each transformer has an encode() and a decode() 22 | method allowing conversion to be done either way. 23 | 24 | encodeOneOf and decodeOneOf allow several Transformers to be composed to allow a value to be encoded 25 | or decoded that matches any one of the transformers. 26 | 27 | E.g. 28 | 29 | Transformer.intAsString will convert integers to and from string representations - allowing large 30 | integers (greater than 53 bits precision) to be represented in JSON as strings without loss of precision. 31 | 32 | Transformer.plusInf and Transformer.minusInf will convert between plus or minus infinity (represented by 33 | a python float value) and the strings "plusinfinity" and "minusinfinity" 34 | """ 35 | 36 | import re 37 | from dvbcss.protocol import OMIT 38 | 39 | _re_intAsString = re.compile('^(0|(-?[1-9])[0-9]*)$') 40 | 41 | def encodeOneOf(value, errMsg, *transformers): 42 | """Attempt to encode value using one of the transformers supplied. They are tried in the order provided. 43 | If all fail, the raise a ValueError with the error message provided.""" 44 | for t in transformers: 45 | try: 46 | return t.encode(value) 47 | except Exception, e: 48 | pass 49 | raise ValueError(errMsg+" Value: "+str(value)+"\nCause: "+str(e)) 50 | 51 | 52 | def decodeOneOf(value, errMsg, *transformers): 53 | """Attempt to decode value using one of the transformers supplied. They are tried in the order provided. 54 | If all fail, the raise a ValueError with the error message provided.""" 55 | for t in transformers: 56 | try: 57 | return t.decode(value) 58 | except Exception: 59 | pass 60 | raise ValueError(errMsg+" Value: "+str(value)) 61 | 62 | 63 | 64 | class Transformer(object): 65 | """\ 66 | Contains set of standard transformer objects that implement a decode(value) and encode(value) methods. 67 | 68 | The purpose of these transformers to to transform to and from a representation suitable for conversion 69 | to json: 70 | 71 | --encode--> --json.dumps--> 72 | Python objects Python primitives JSON 73 | <--decode-- <--json.loads-- 74 | """ 75 | 76 | class intAsString(object): 77 | """\ 78 | Transformer object with methods: 79 | 80 | encode(value) : 12345 -> "12345" 81 | decode(value) : "12345" -> 12345 82 | 83 | Raises ValueError if input value is not expected form. 84 | """ 85 | @classmethod 86 | def decode(cls,value): 87 | match = _re_intAsString.match(value) 88 | if match: 89 | return int(match.group(1)) 90 | else: 91 | raise ValueError("Format of time value not recognised: "+str(value)) 92 | @classmethod 93 | def encode(cls,value): 94 | return str(int(value)) 95 | 96 | class minusInf(object): 97 | """\ 98 | Transformer object with methods: 99 | 100 | encode(value) : float("-inf") -> "minusinfinity" 101 | decode(value) : "minusinfinity" -> float("-inf") 102 | 103 | Raises ValueError if input value is not expected form. 104 | """ 105 | @classmethod 106 | def decode(cls,value): 107 | if value == "minusinfinity": 108 | return float("-inf") 109 | else: 110 | raise ValueError("Value is not encoded as minus infinity") 111 | @classmethod 112 | def encode(cls,value): 113 | if value == float("-inf"): 114 | return "minusinfinity" 115 | else: 116 | raise ValueError("Isn't minus infinity") 117 | 118 | class plusInf(object): 119 | """\ 120 | Transformer object with methods: 121 | 122 | encode(value) : float("+inf") -> "plusinfinity" 123 | decode(value) : "plusinfinity" -> float("+inf") 124 | 125 | Raises ValueError if input value is not expected form. 126 | """ 127 | @classmethod 128 | def decode(cls,value): 129 | if value == "plusinfinity": 130 | return float("+inf") 131 | else: 132 | raise ValueError("Value is not encoded as plus infinity") 133 | @classmethod 134 | def encode(cls,value): 135 | if value == float("+inf"): 136 | return "plusinfinity" 137 | else: 138 | raise ValueError("Isn't plus infinity") 139 | 140 | class float(object): 141 | """\ 142 | Transformer object with methods: 143 | 144 | encode(value) : 1234.5 -> 1234.5 145 | decode(value) : 1234.5 -> 1234.5 146 | 147 | Raises ValueError if input value is not expected form. 148 | """ 149 | 150 | @classmethod 151 | def decode(cls,value): 152 | return float(value) 153 | @classmethod 154 | def encode(cls,value): 155 | return float(value) 156 | 157 | class null(object): 158 | """\ 159 | Transformer object with methods: 160 | 161 | encode(value) : None -> None 162 | decode(value) : None -> None 163 | 164 | Raises ValueError if input value is not expected form. 165 | """ 166 | @classmethod 167 | def decode(cls,value): 168 | if value is None: 169 | return value 170 | else: 171 | raise ValueError("Value is not null") 172 | @classmethod 173 | def encode(cls,value): 174 | if value is None: 175 | return value 176 | else: 177 | raise ValueError("Value is not None") 178 | 179 | class uriString(object): 180 | """\ 181 | Transformer object with methods: 182 | 183 | encode(value) : uri-string -> uri-string 184 | decode(value) : uri-string -> uri-string 185 | 186 | Raises ValueError if input value is not expected form. 187 | """ 188 | 189 | # Regular expression from RFC3986 appendix B 190 | _uriRegex = re.compile("^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?") 191 | 192 | @classmethod 193 | def decode(cls,value): 194 | if cls._uriRegex.match(value): 195 | return value 196 | else: 197 | raise ValueError("Value is not a valid URI: "+str(value)) 198 | 199 | encode=decode 200 | 201 | class omit(object): 202 | """\ 203 | Transformer object with methods: 204 | 205 | encode(value) : OMIT -> OMIT 206 | decode(value) : OMIT -> OMIT 207 | 208 | Raises ValueError if input value is not expected form. 209 | """ 210 | @classmethod 211 | def decode(cls,value): 212 | if value == OMIT: 213 | return value 214 | else: 215 | raise ValueError("Value is not OMIT") 216 | @classmethod 217 | def encode(cls,value): 218 | if value == OMIT: 219 | return value 220 | else: 221 | raise ValueError("Value is not OMIT") 222 | 223 | @classmethod 224 | def listOf(cls, transformer): 225 | class listOf(object): 226 | @classmethod 227 | def decode(cls,value): 228 | try: 229 | return [transformer.decode(x) for x in value] 230 | except: 231 | raise ValueError("Value is not a list of the correct type of items.") 232 | @classmethod 233 | def encode(cls,value): 234 | try: 235 | return [transformer.encode(x) for x in value] 236 | except: 237 | raise ValueError("Value is not a list of the correct type of items.") 238 | return listOf 239 | 240 | @classmethod 241 | def matchOneOf(cls,*items): 242 | class matchOneOf(object): 243 | @classmethod 244 | def decode(cls,value): 245 | if value in items: 246 | return value 247 | else: 248 | raise ValueError("Value is not one from the list "+repr(items)) 249 | @classmethod 250 | def encode(cls,value): 251 | if value in items: 252 | return value 253 | else: 254 | raise ValueError("Value is not one from the list "+repr(items)) 255 | return matchOneOf 256 | 257 | class private(object): 258 | """Transformer object with methods: 259 | 260 | encode(value) : [ private, private, ...] -> [ private, private, ...] 261 | decode(value) : [ private, private, ...] -> [ private, private, ...] 262 | 263 | where private = { "type" : uriString, } 264 | """ 265 | @classmethod 266 | def decode(cls, value): 267 | try: 268 | for item in value: 269 | Transformer.uriString.decode(item["type"]) 270 | return value 271 | except (ValueError, KeyError), e: 272 | raise ValueError("Not a valid private structure:"+str(e)) 273 | 274 | encode=decode 275 | 276 | -------------------------------------------------------------------------------- /dvbcss/util.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2015 British Broadcasting Corporation 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import socket 18 | import random 19 | import re 20 | import logging 21 | """ 22 | class Sink(object): 23 | def write(self, *args, **kwargs): 24 | pass 25 | def writeln(self, *args, **kwargs): 26 | pass 27 | 28 | 29 | class Base(object): 30 | def __init__(self, **kwargs): 31 | super(Base,self).__init__() 32 | if "logTo" in kwargs: 33 | if kwargs["logTo"] == None: 34 | self.log = Sink() 35 | self.errlog = Sink() 36 | else: 37 | self.log = kwargs["logTo"] 38 | self.errlog = kwargs["logTo"] 39 | else: 40 | self.log = sys.stdout 41 | self.errlog = sys.stderr 42 | """ 43 | def wsUrl_str(t): 44 | if t.startswith("ws://"): 45 | return t 46 | else: 47 | raise ValueError("Not a ws:// format url") 48 | 49 | def udpUrl_str(t): 50 | m = re.match("^udp://([^:/]+):([0-9]+)$",t) 51 | if m: 52 | return iphost_str(m.group(1)), int(m.group(2)) 53 | else: 54 | raise ValueError("Not a udp://: url") 55 | 56 | def iphost_str(t): 57 | """Pass through a string provide it represents an ip address, or convert from a host name to string representation of ip address 58 | otherwise raise an exception if not possible""" 59 | try: 60 | socket.inet_aton(t) 61 | return t 62 | except socket.error: 63 | try: 64 | return socket.gethostbyname(t) 65 | except: 66 | raise ValueError("Not a recognised/resolvable host name or is an IP address string not of the form nnn.nnn.nnn.nnn") 67 | 68 | def ipaddr_str(t): 69 | """Pass through a string provided it represents an ip address (not a host name) otherwise raise an exception""" 70 | try: 71 | socket.inet_aton(t) 72 | except socket.error: 73 | raise ValueError("IP address string not of the form nnn.nnn.nnn.nnn") 74 | return t 75 | 76 | 77 | def port_int(p): 78 | """Pass through a port number (as a number or a string) provided it is valid and in range, otherwise raise an exception""" 79 | try: 80 | port=int(p) 81 | except: 82 | raise ValueError("Invalid port number") 83 | 84 | if port>=0 and port<=65535: 85 | return port 86 | else: 87 | raise ValueError("Port number out of range") 88 | 89 | def port_int_or_random(p): 90 | """Same as port_int() except will also accept "random" in which case a random port between 10000 and 20000 is chosen""" 91 | if p=="random": 92 | return random.randrange(10000,20000) 93 | else: 94 | return port_int(p) 95 | 96 | def parse_logLevel(levelName): 97 | levelName=levelName.lower() 98 | for n in range(logging.NOTSET,logging.CRITICAL+1): 99 | name=logging.getLevelName(n).lower() 100 | if levelName == name: 101 | return n 102 | raise ValueError("Logging level name not recognised") 103 | -------------------------------------------------------------------------------- /examples/CIIClient.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2015 British Broadcasting Corporation 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | """\ 18 | .. py:module:: examples.CIIClient 19 | 20 | It works by instantiating a :class:`~dvbcss.protocol.client.cii.CIIClient` 21 | and attaching handler 22 | functions to be notified of when connection and disconnection occurs 23 | and of changes to the CII information being pushed from the server. 24 | 25 | At the command line you must specify: 26 | 27 | * the WebSocket URL of the CSS-CII server, in the form `ws://:/` 28 | 29 | Command line options can be used to reduce the amount of logging output. 30 | 31 | Use the ``--help`` command line option for usage information. 32 | """ 33 | 34 | def makeCallback(msg): 35 | """\ 36 | Factory for creating callback handler functions that display a specified message when called. 37 | 38 | :param msg: The message string to display when the handler is called. 39 | :returns: a callback handler function. 40 | """ 41 | def callback(*a,**k): 42 | ciiClientLogger.info(msg) 43 | return callback 44 | 45 | def makePropertyChangeCallback(name): 46 | """\ 47 | Factory for creating a callback handler specific to callbacks used to notify when a CII property changes value. 48 | 49 | :param name: The name (string) of the property 50 | :returns: a callback handler function. 51 | """ 52 | def callback(value): 53 | ciiClientLogger.info("change to "+name+" property. Value is now: " + str(value)) 54 | return callback 55 | 56 | 57 | if __name__ == "__main__": 58 | 59 | import _useDvbCssUninstalled # Enable to run when dvbcss not yet installed ... @UnusedImport 60 | 61 | import argparse 62 | import dvbcss.monotonic_time as time 63 | import logging 64 | from dvbcss.protocol.cii import CII 65 | from dvbcss.protocol.client.cii import CIIClient 66 | import dvbcss.util 67 | 68 | parser=argparse.ArgumentParser( 69 | description="Client for connecting to CSS-CII server.") 70 | 71 | parser.add_argument("-q","--quiet",dest="quiet",action="store_true",default=False,help="Suppress extraneous output during runtime. Overrides loglevel option") 72 | parser.add_argument("--loglevel",dest="loglevel",action="store",type=dvbcss.util.parse_logLevel, nargs=1, help="Set logging level to one of: critical, error, warning, info, debug. Default=info",default=[logging.INFO]) 73 | parser.add_argument("ciiUrl", action="store", type=dvbcss.util.wsUrl_str, nargs=1, help="ws:// URL of CSS-CII end point") 74 | args = parser.parse_args() 75 | 76 | ciiUrl = args.ciiUrl[0] 77 | 78 | if args.quiet: 79 | logging.disable(logging.CRITICAL) 80 | else: 81 | logging.basicConfig(level=args.loglevel[0]) 82 | 83 | 84 | cii = CIIClient(ciiUrl) 85 | 86 | # logger for outputting messages 87 | ciiClientLogger = logging.getLogger("CIIClient") 88 | 89 | # attach callbacks to generate notifications 90 | cii.onConnected = makeCallback("connected") 91 | cii.onDisconnected = makeCallback("disconnected") 92 | cii.onError = makeCallback("error"); 93 | 94 | for name in CII.allProperties(): 95 | funcname="on" + name[0].upper() + name[1:] + "Change" 96 | callback = makePropertyChangeCallback(name) 97 | setattr(cii, funcname, callback) 98 | 99 | # specific handler for when a CII 'change' notification callback fires 100 | def onChange(changes): 101 | ciiClientLogger.info("CII is now: "+str(cii.cii)) 102 | 103 | cii.onChange = onChange 104 | 105 | # connect and goto sleep. All callback activity happens in the websocket's own thread 106 | cii.connect() 107 | while True: 108 | time.sleep(1) 109 | -------------------------------------------------------------------------------- /examples/CIIServer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2015 British Broadcasting Corporation 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | """\ 18 | .. py:module:: examples.CIIServer 19 | 20 | It works by setting up a web server and the ws4py plug-in for cherrypy that provides 21 | WebSockets support. 22 | It then instantiates a :class:`~dvbcss.protocol.server.cii.CIIServer` 23 | and mounts it into the cherrypy server at the URL resource path "/cii". 24 | 25 | While the server is running, it pretends to be hopping between a few different broadcast 26 | channels every 7 seconds, with a 2 second "transitioning" period on each hop. 27 | 28 | This is an artificially simple example and does not provide values for most properties of 29 | the CII message - such as a MRS URL, or any URLs for a WC or TS endpoints. 30 | 31 | It does not do any media presentation, but just provides a CSS-CII server with some pretend data. 32 | 33 | This server, by default, serves on port 7681 and provides a CSS-CII service at the 34 | URL resource path ``/cii``. It can therefore be connected to using 35 | the WebSocket URL "ws://:7681/cii" e.g. "ws://127.0.0.1:7681/cii". 36 | Command line options can be used to override these defaults and to reduce the amount of logging output. 37 | 38 | Use the ``--help`` command line option for more detailed usage information. 39 | 40 | """ 41 | if __name__ == "__main__": 42 | 43 | import _useDvbCssUninstalled # Enable to run when dvbcss not yet installed ... @UnusedImport 44 | 45 | import cherrypy 46 | from dvbcss.protocol.server.cii import CIIServer 47 | import logging 48 | from ws4py.server.cherrypyserver import WebSocketPlugin 49 | import dvbcss.monotonic_time as time 50 | 51 | import argparse 52 | import dvbcss.util 53 | 54 | DEFAULT_BIND=("127.0.0.1",7681) 55 | 56 | parser=argparse.ArgumentParser(description="Simple example CSS-CII server.") 57 | 58 | parser.add_argument("-q","--quiet",dest="quiet",action="store_true",default=False,help="Suppress all output during runtime. Overrides loglevel option") 59 | parser.add_argument("-Q","--quiet-cherrypy",dest="quiet_cp",action="store_true",default=False,help="Quieten cherrypy only") 60 | parser.add_argument("--loglevel",dest="loglevel",action="store",type=dvbcss.util.parse_logLevel, nargs=1, help="Set logging level to one of: critical, error, warning, info, debug. Default=info",default=[logging.INFO]) 61 | parser.add_argument("cii_addr",action="store", type=dvbcss.util.iphost_str, nargs="?",help="IP address or host name to bind to (default="+str(DEFAULT_BIND[0])+")",default=DEFAULT_BIND[0]) 62 | parser.add_argument("cii_port",action="store", type=dvbcss.util.port_int, nargs="?",help="Port number to bind to (default="+str(DEFAULT_BIND[1])+")",default=DEFAULT_BIND[1]) 63 | args = parser.parse_args() 64 | 65 | if args.quiet: 66 | logging.disable(logging.CRITICAL) 67 | else: 68 | logging.basicConfig(level=args.loglevel[0]) 69 | 70 | if args.quiet_cp: 71 | cherrypy.log.error_file = "" 72 | cherrypy.log.access_file = "" 73 | logging.getLogger("cherrypy.error").setLevel(logging.CRITICAL) 74 | 75 | if not args.quiet: 76 | print "----" 77 | print "CSS-CII endpoint URL: ws://%s:%d/cii" % (args.cii_addr, args.cii_port) 78 | print "----" 79 | 80 | WebSocketPlugin(cherrypy.engine).subscribe() 81 | 82 | cherrypy.config.update({"server.socket_host":args.cii_addr}) 83 | cherrypy.config.update({"server.socket_port":args.cii_port}) 84 | cherrypy.config.update({"engine.autoreload.on":False}) 85 | 86 | ciiServer = CIIServer(maxConnectionsAllowed=2) 87 | 88 | class Root(object): 89 | @cherrypy.expose 90 | def cii(self): 91 | pass 92 | 93 | cherrypy.tree.mount(Root(), "/", config={"/cii": {'tools.dvb_cii.on': True, 94 | 'tools.dvb_cii.handler_cls': ciiServer.handler}}) 95 | 96 | cherrypy.engine.start() 97 | 98 | 99 | dummyContentIds = [ 100 | "dvb://233a.1004.1044;363a~20130218T0915Z--PT00H45M", 101 | "dvb://233a.1168.122a;1aa4~20130218T0910Z--PT00H30M", 102 | "dvb://233a.1050.1008;9fd~20130218T0920Z--PT00H50M", 103 | ] 104 | 105 | # we're going to pretend to be hopping channels every few seconds, 106 | # going through a brief 2 second "transitioniong" presentationStatus 107 | 108 | ciiServer.updateClients(sendOnlyDiff=True) 109 | try: 110 | while True: 111 | for contentId in dummyContentIds: 112 | ciiServer.cii.contentId = contentId 113 | ciiServer.cii.contentIdStatus = "final" 114 | ciiServer.cii.presentationStatus = ["transitioning"] 115 | ciiServer.updateClients(sendOnlyDiff=True) 116 | 117 | time.sleep(2) 118 | ciiServer.cii.contentIdStatus = "final" 119 | ciiServer.cii.presentationStatus = ["okay"] 120 | ciiServer.updateClients(sendOnlyDiff=True) 121 | 122 | time.sleep(5) 123 | except KeyboardInterrupt: 124 | pass 125 | finally: 126 | cherrypy.engine.exit() 127 | 128 | 129 | -------------------------------------------------------------------------------- /examples/TSClient.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2015 British Broadcasting Corporation 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | """\ 18 | .. py:module:: examples.TSClient 19 | 20 | It works by implementing `both` a wall clock client and a CSS-TS client. A 21 | :class:`~dvbcss.protocol.client.ts.TSClientClockController` object is instantiated 22 | and provided with a :class:`~dvbcss.clock.CorrelatedClock` object to represent 23 | the synchronisation timeline. The controller adjusts the clock object to match 24 | the timeline information coming from the server. 25 | 26 | At the command line you must specify: 27 | 28 | * the WebSocket URL of the CSS-TS server, in the form `ws://:/` 29 | * a `udp://:` format URL for the Wall Clock server 30 | * The content ID stem and timeline selector to be used when requesting the timeline 31 | * The tick rate of the timeline. 32 | 33 | Default options can be overridden for the IP address and port that the Wall Clock client 34 | binds to and to reduce the amount of logging output. 35 | 36 | Use the ``--help`` command line option for usage information. 37 | 38 | """ 39 | 40 | if __name__ == "__main__": 41 | 42 | import _useDvbCssUninstalled # Enable to run when dvbcss not yet installed ... @UnusedImport 43 | 44 | from dvbcss.clock import SysClock 45 | from dvbcss.clock import CorrelatedClock 46 | from dvbcss.clock import Correlation 47 | from dvbcss.protocol.client.wc import WallClockClient 48 | from dvbcss.protocol.client.wc.algorithm import LowestDispersionCandidate 49 | 50 | from dvbcss.protocol.client.ts import TSClientClockController 51 | 52 | import dvbcss.monotonic_time as time 53 | import logging 54 | import argparse 55 | import dvbcss.util 56 | import sys 57 | 58 | DEFAULT_WC_BIND=("0.0.0.0","random") 59 | 60 | parser=argparse.ArgumentParser( 61 | description="Run a DVB TM-CSS Wall Clock Client (WC-Client) and connection to CSS-TS to obtain a timeline.") 62 | 63 | parser.add_argument("-q","--quiet",dest="quiet",action="store_true",default=False,help="Suppress extraneous output during runtime. Overrides loglevel option") 64 | parser.add_argument("--loglevel",dest="loglevel",action="store",type=dvbcss.util.parse_logLevel, nargs=1, help="Set logging level to one of: critical, error, warning, info, debug. Default=info",default=[logging.INFO]) 65 | parser.add_argument("--wcloglevel",dest="wcloglevel",action="store",type=dvbcss.util.parse_logLevel, nargs=1, help="Set logging level for the wall clock client to one of: critical, error, warning, info, debug. Default=warn",default=[logging.WARN]) 66 | parser.add_argument("tsUrl", action="store", type=dvbcss.util.wsUrl_str, nargs=1, help="ws:// URL of CSS-TS end point") 67 | parser.add_argument("wcUrl", action="store", type=dvbcss.util.udpUrl_str, nargs=1, help="udp://: URL of CSS-WC end point") 68 | parser.add_argument("contentIdStem", action="store", type=str, nargs=1, help="contentIdStem") 69 | parser.add_argument("timelineSelector", action="store", type=str, nargs=1, help="Timeline selector") 70 | parser.add_argument("timelineFreq", action="store", type=int, nargs=1, help="Ticks per second of the media timeline") 71 | parser.add_argument("wc_bindaddr",action="store", type=dvbcss.util.iphost_str, nargs="?",help="IP address or host name to bind WC client to (default="+str(DEFAULT_WC_BIND[0])+")",default=DEFAULT_WC_BIND[0]) 72 | parser.add_argument("wc_bindport",action="store", type=dvbcss.util.port_int_or_random, nargs="?",help="Port number to bind WC client to (default="+str(DEFAULT_WC_BIND[1])+")",default=DEFAULT_WC_BIND[1]) 73 | args = parser.parse_args() 74 | 75 | tsUrl = args.tsUrl[0] 76 | wc_dest=args.wcUrl[0] 77 | wc_bind=(args.wc_bindaddr, args.wc_bindport) 78 | contentIdStem = args.contentIdStem[0] 79 | timelineSelector= args.timelineSelector[0] 80 | timelineFreq = args.timelineFreq[0] 81 | 82 | if args.quiet: 83 | logging.disable(logging.CRITICAL) 84 | else: 85 | logging.basicConfig(level=args.loglevel[0]) 86 | 87 | logging.getLogger("dvbcss.protocol.client.wc").setLevel(args.wcloglevel[0]) 88 | 89 | sysclock=SysClock() 90 | wallClock=CorrelatedClock(sysclock,tickRate=1000000000) # nanos 91 | 92 | algorithm = LowestDispersionCandidate(wallClock,repeatSecs=1,timeoutSecs=0.5) 93 | 94 | wc_client=WallClockClient(wc_bind, wc_dest, wallClock, algorithm) 95 | wc_client.start() 96 | 97 | timelineClock = CorrelatedClock(wallClock, timelineFreq) 98 | timelineClock.setAvailability(False) 99 | 100 | print "Connecting, requesting timeline for:" 101 | print " Any contentId beginning with:",contentIdStem 102 | print " and using timeline selector: ",timelineSelector 103 | print 104 | 105 | ts = TSClientClockController(tsUrl, contentIdStem, timelineSelector, timelineClock, correlationChangeThresholdSecs=0.001) 106 | 107 | exiting=False 108 | tsClientLogger = logging.getLogger("TSClient") 109 | def reportCallback(msg,exit=False): 110 | def callback(*a,**k): 111 | global exiting 112 | tsClientLogger.info(msg+"\n") 113 | if exit: 114 | exiting=True 115 | wc_client.stop() 116 | sys.exit(0) 117 | return callback 118 | 119 | ts.onConnected = reportCallback("connected") 120 | ts.onDisconnected = reportCallback("disconnected",exit=True) 121 | ts.onTimelineAvailable = reportCallback("timeline became available") 122 | ts.onTimelineUnavailable = reportCallback("timeline became un-available") 123 | ts.onTimingChange = reportCallback("change in timing and/or play speed") 124 | 125 | ts.connect() 126 | while not exiting: 127 | time.sleep(0.4) 128 | print ts.getStatusSummary(), 129 | print " Uncertainty (dispersion) = +/- %0.3f milliseconds" % (algorithm.getCurrentDispersion()/1000000.0) 130 | -------------------------------------------------------------------------------- /examples/TVDevice.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2015 British Broadcasting Corporation 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | """\ 18 | .. py:module:: examples.TVDevice 19 | 20 | This example works by setting up a web server and the ws4py plug-in for cherrypy that provides 21 | WebSockets support. 22 | It then instantiates a :class:`~dvbcss.protocol.server.ts.TSServer` and 23 | :class:`~dvbcss.protocol.server.cii.CIIServer` and mounts it into the cherrypy server. 24 | It also includes a wall clock server. 25 | 26 | It does not play any media, but instead serves an imaginary set of timelines and 27 | pretends to be presenting a broadcast service. 28 | 29 | It creates :mod:`~dvbcss.clock` objects to represent timelines and the wall clock. 30 | :class:`~dvbcss.protocol.server.ts.SimpleClockTimelineSource` objects are used to interface the clocks as 31 | sources of timelines to the TS server object. 32 | 33 | It has a hardcoded DVB URL as the content ID (displayed when you start it running) 34 | and provides the following timelines: 35 | 36 | * `urn:dvb:css:timeline:pts` ... a PTS timeline 37 | * `urn:dvb:css:timelime:temi:1:1` ... a TEMI timeline ticking at 1kHz that toggles availability every 30 seconds 38 | 39 | The PTS and TEMI timelines both start ticking up from zero the moment the server starts. 40 | 41 | By default, this server binds to 0.0.0.0 on port 7681 and provides a CSS-CII service at the 42 | URL `ws://{host}:7681/cii` and a CSS-TS service at the 43 | URL `ws://{host}:7681/ts`. It also provides a wall clock server bound to 0.0.0.0 on UDP port 6677. 44 | 45 | The CII service will provide URLs for the TS and WC endpoints that match the host name on which the 46 | 47 | Command line options can be used to override these defaults and to reduce the amount of logging output. 48 | 49 | Use the ``--help`` command line option for more detailed usage information. 50 | """ 51 | 52 | if __name__ == '__main__': 53 | 54 | import _useDvbCssUninstalled # Enable to run when dvbcss not yet installed ... @UnusedImport 55 | 56 | import logging 57 | import dvbcss.monotonic_time as time 58 | 59 | from dvbcss.clock import SysClock, CorrelatedClock, Correlation 60 | 61 | from dvbcss.protocol import OMIT 62 | from dvbcss.protocol.cii import CII, TimelineOption 63 | from dvbcss.protocol.server.cii import CIIServer 64 | from dvbcss.protocol.server.ts import TSServer, SimpleClockTimelineSource 65 | from dvbcss.protocol.server.wc import WallClockServer 66 | 67 | import cherrypy 68 | from ws4py.server.cherrypyserver import WebSocketPlugin 69 | 70 | import argparse 71 | import dvbcss.util 72 | 73 | DEFAULT_WS_BIND=("0.0.0.0",7681) 74 | DEFAULT_WC_BIND=("0.0.0.0",6677) 75 | 76 | CONTENT_ID = "dvb://233a.1004.1044;363a~20130218T0915Z--PT00H45M" 77 | 78 | parser=argparse.ArgumentParser(description="Simple example CSS-TS server (and CSS-WC server).") 79 | 80 | parser.add_argument("-q","--quiet",dest="quiet",action="store_true",default=False,help="Suppress all output during runtime. Overrides loglevel option") 81 | parser.add_argument("-Q","--quiet-cherrypy",dest="quiet_cp",action="store_true",default=False,help="Quieten cherrypy only") 82 | parser.add_argument("--loglevel",dest="loglevel",action="store",type=dvbcss.util.parse_logLevel, nargs=1, help="Set logging level to one of: critical, error, warning, info, debug. Default=info",default=[logging.INFO]) 83 | parser.add_argument("ws_addr",action="store", type=dvbcss.util.iphost_str, nargs="?",help="IP address or host name to bind to for the CSS-CII and CSS-TS servers (default="+str(DEFAULT_WS_BIND[0])+")",default=DEFAULT_WS_BIND[0]) 84 | parser.add_argument("ws_port",action="store", type=dvbcss.util.port_int, nargs="?",help="Port number to bind to for the CSS-CII and CSS-TS servers (default="+str(DEFAULT_WS_BIND[1])+")",default=DEFAULT_WS_BIND[1]) 85 | parser.add_argument("wc_addr",action="store", type=dvbcss.util.iphost_str, nargs="?",help="IP address or host name to bind to for the CSS-WC server (default="+str(DEFAULT_WC_BIND[0])+")",default=DEFAULT_WC_BIND[0]) 86 | parser.add_argument("wc_port",action="store", type=dvbcss.util.port_int, nargs="?",help="Port number to bind to for the CSS-WC server (default="+str(DEFAULT_WC_BIND[1])+")",default=DEFAULT_WC_BIND[1]) 87 | args = parser.parse_args() 88 | 89 | if args.quiet: 90 | logging.disable(logging.CRITICAL) 91 | else: 92 | logging.basicConfig(level=args.loglevel[0]) 93 | 94 | if args.quiet_cp: 95 | cherrypy.log.error_file = "" 96 | cherrypy.log.access_file = "" 97 | logging.getLogger("cherrypy.error").setLevel(logging.CRITICAL) 98 | 99 | if not args.quiet: 100 | print "----" 101 | print "CSS-CII endpoint URL: ws://%s:%d/cii" % (args.ws_addr, args.ws_port) 102 | print "CSS-TS endpoint URL: ws://%s:%d/ts" % (args.ws_addr, args.ws_port) 103 | print "CSS-WC endpoint bound to host %s on port %d" % (args.wc_addr, args.wc_port) 104 | print "Content ID:",CONTENT_ID 105 | print "----" 106 | 107 | 108 | WebSocketPlugin(cherrypy.engine).subscribe() 109 | 110 | 111 | systemClock= SysClock(tickRate=1000000000, maxFreqErrorPpm = 500) 112 | wallClock = CorrelatedClock(parentClock=systemClock, tickRate=1000000000, correlation=Correlation(0,0)) 113 | 114 | cherrypy.config.update({"server.socket_host":args.ws_addr}) 115 | cherrypy.config.update({"server.socket_port":args.ws_port}) 116 | cherrypy.config.update({"engine.autoreload.on":False}) 117 | 118 | wcServer = WallClockServer(wallClock, None, None, bindaddr=args.wc_addr, bindport=args.wc_port) 119 | 120 | ciiServer = CIIServer(maxConnectionsAllowed=5, enabled=True, rewriteHostPort=['wcUrl','tsUrl']) 121 | tsServer = TSServer(CONTENT_ID, wallClock, maxConnectionsAllowed=10, enabled=True) 122 | 123 | class Root(object): 124 | @cherrypy.expose 125 | def cii(self): 126 | pass 127 | 128 | @cherrypy.expose 129 | def ts(self): 130 | pass 131 | 132 | cherrypy.tree.mount(Root(), "/", config={"/cii": {'tools.dvb_cii.on': True, 133 | 'tools.dvb_cii.handler_cls': ciiServer.handler}, 134 | 135 | "/ts": {'tools.dvb_ts.on': True, 136 | 'tools.dvb_ts.handler_cls': tsServer.handler} 137 | }) 138 | 139 | ciiServer.cii = CII( 140 | protocolVersion="1.1", 141 | contentId=CONTENT_ID, 142 | contentIdStatus="final", 143 | presentationStatus=["okay"], 144 | mrsUrl=OMIT, 145 | tsUrl="ws://{{host}}:{{port}}/ts", # host & port rewriting has been enabled on the CII server 146 | wcUrl="udp://{{host}}:%d" % args.wc_port, # host & port rewriting has been enabled on the CII server 147 | teUrl=OMIT, 148 | timelines = [ 149 | TimelineOption("urn:dvb:css:timeline:pts", unitsPerTick=1, unitsPerSecond=90000), 150 | TimelineOption("urn:dvb:css:timeline:temi:1:1", unitsPerTick=1, unitsPerSecond=50) 151 | ] 152 | ) 153 | 154 | ptsTimeline = CorrelatedClock(parentClock=wallClock, tickRate=90000, correlation=Correlation(wallClock.ticks, 0)) 155 | temiTimeline = CorrelatedClock(parentClock=ptsTimeline, tickRate=50, correlation=Correlation(0,0)) 156 | 157 | ptsSource = SimpleClockTimelineSource("urn:dvb:css:timeline:pts", wallClock=wallClock, clock=ptsTimeline, speedSource=ptsTimeline) 158 | temiSource = SimpleClockTimelineSource("urn:dvb:css:timeline:temi:1:1", wallClock=wallClock, clock=temiTimeline, speedSource=ptsTimeline) 159 | 160 | tsServer.attachTimelineSource(ptsSource) 161 | tsServer.attachTimelineSource(temiSource) 162 | 163 | wcServer.start() 164 | 165 | cherrypy.engine.start() 166 | 167 | try: 168 | while True: 169 | time.sleep(30) 170 | temiTimeline.setAvailability(False) 171 | tsServer.updateAllClients() 172 | 173 | time.sleep(30) 174 | temiTimeline.setAvailability(True) 175 | tsServer.updateAllClients() 176 | 177 | except KeyboardInterrupt: 178 | pass 179 | finally: 180 | cherrypy.engine.exit() 181 | wcServer.stop() 182 | 183 | 184 | -------------------------------------------------------------------------------- /examples/WallClockClient.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2015 British Broadcasting Corporation 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | """\ 18 | .. py:module:: examples.WallClockClient 19 | 20 | It works by instantiating a :class:`~dvbcss.protocol.client.wc.WallClockClient` 21 | object and plugs into that object a 22 | :class:`~dvbcss.protocol.client.wc.algorithm.LowestDispersionCandidate` algorithm 23 | object that adjusts a :class:`~dvbcss.clock.TunableClock` representing the Wall Clock. 24 | 25 | At the command line 26 | you must specify the host and port of the Wall Clock server. Default options 27 | can be overridden for the IP address and port that the client listens on. 28 | 29 | Use the ``--help`` command line option for usage information. 30 | """ 31 | 32 | import _useDvbCssUninstalled # Enable to run when dvbcss not yet installed ... @UnusedImport 33 | 34 | 35 | 36 | if __name__ == "__main__": 37 | from dvbcss.clock import SysClock as SysClock 38 | from dvbcss.clock import CorrelatedClock as CorrelatedClock 39 | from dvbcss.protocol.client.wc import WallClockClient 40 | from dvbcss.protocol.client.wc.algorithm import LowestDispersionCandidate 41 | 42 | import dvbcss.util 43 | 44 | import argparse 45 | import logging 46 | import dvbcss.monotonic_time as time 47 | 48 | 49 | DEFAULT_BIND=("0.0.0.0","random") 50 | DEFAULT_DEST=("127.0.0.1",6677) 51 | 52 | parser=argparse.ArgumentParser( 53 | description="Run a DVB TM-CSS Wall Clock Client (WC-Client).") 54 | 55 | parser.add_argument("-q","--quiet",dest="quiet",action="store_true",default=False,help="Suppress extraneous output during runtime. Overrides loglevel option") 56 | parser.add_argument("--loglevel",dest="loglevel",action="store",type=dvbcss.util.parse_logLevel, nargs=1, help="Set logging level to one of: critical, error, warning, info, debug. Default=debug",default=[logging.DEBUG]) 57 | parser.add_argument("addr",action="store", type=dvbcss.util.iphost_str, nargs="?",help="IP address or host name of server (default="+str(DEFAULT_DEST[0])+")",default=DEFAULT_DEST[0]) 58 | parser.add_argument("port",action="store", type=dvbcss.util.port_int, nargs="?",help="Port number of server (default="+str(DEFAULT_DEST[1])+")",default=DEFAULT_DEST[1]) 59 | parser.add_argument("bindaddr",action="store", type=dvbcss.util.iphost_str, nargs="?",help="IP address or host name to bind to (default="+str(DEFAULT_BIND[0])+")",default=DEFAULT_BIND[0]) 60 | parser.add_argument("bindport",action="store", type=dvbcss.util.port_int_or_random, nargs="?",help="Port number to bind to (default="+str(DEFAULT_BIND[1])+")",default=DEFAULT_BIND[1]) 61 | args = parser.parse_args() 62 | 63 | dest=(args.addr, args.port) 64 | bind=(args.bindaddr, args.bindport) 65 | 66 | if args.quiet: 67 | logging.disable(logging.CRITICAL) 68 | else: 69 | logging.basicConfig(level=args.loglevel[0]) 70 | 71 | #first we'll create a clock to represent the wall clock 72 | sysclock=SysClock(tickRate=1000000000) 73 | wallClock=CorrelatedClock(sysclock,tickRate=1000000000) 74 | 75 | # we'll also create the algorithm object that adjusts the clock and controls 76 | # how often requests are made to the server. 77 | algorithm = LowestDispersionCandidate(wallClock,repeatSecs=1,timeoutSecs=0.5) 78 | 79 | # finally we create the client and start it. 80 | wc_client=WallClockClient(bind, dest, wallClock, algorithm) 81 | wc_client.start() 82 | 83 | n=0 84 | while True: 85 | time.sleep(0.2) 86 | print "Time=%20d microseconds. Dispersion = %15.3f milliseconds" % (wallClock.ticks*1000000/wallClock.tickRate, wc_client.algorithm.getCurrentDispersion()/1000000) 87 | n=n+1 88 | if n>=25: 89 | print "*** Worst dispersion over previous 5 seconds = %15.3f milliseconds" % (wc_client.algorithm.getWorstDispersion()/1000000) 90 | n=0 91 | -------------------------------------------------------------------------------- /examples/WallClockServer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2015 British Broadcasting Corporation 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | """\ 18 | .. py:module:: examples.WallClockServer 19 | 20 | It works by instantiating a :class:`~dvbcss.protocol.server.wc.WallClockServer` 21 | object and providing that object with a :mod:`~dvbcss.clock` object to be used 22 | as the Wall Clock that is to be served. 23 | 24 | At the command line you can override default options for 25 | the ip address and port the server binds to; the 26 | maximum frequency error it reports and whether it sends "follow-up" responses 27 | to requests. 28 | 29 | Use the ``--help`` command line option for usage information. 30 | 31 | """ 32 | 33 | import _useDvbCssUninstalled # Enable to run when dvbcss not yet installed ... @UnusedImport 34 | 35 | 36 | if __name__ == '__main__': 37 | from dvbcss.clock import SysClock 38 | from dvbcss.protocol.server.wc import WallClockServer 39 | import dvbcss.util 40 | 41 | import dvbcss.monotonic_time as time 42 | import argparse 43 | import logging 44 | 45 | DEFAULT_BIND=("0.0.0.0",6677) 46 | DEFAULT_PPM=500 47 | 48 | parser=argparse.ArgumentParser( 49 | description="Run a DVB TM-CSS Wall Clock Server (WC-Server).") 50 | 51 | parser.add_argument("-q","--quiet",dest="quiet",action="store_true",default=False,help="Suppress all output during runtime. Overrides loglevel option") 52 | parser.add_argument("--loglevel",dest="loglevel",action="store",type=dvbcss.util.parse_logLevel, nargs=1, help="Set logging level to one of: critical, error, warning, info, debug. Default=info",default=[logging.INFO]) 53 | parser.add_argument("wc_addr",action="store", type=dvbcss.util.iphost_str, nargs="?",help="IP address or host name to bind to (default="+str(DEFAULT_BIND[0])+")",default=DEFAULT_BIND[0]) 54 | parser.add_argument("wc_port",action="store", type=dvbcss.util.port_int, nargs="?",help="Port number to bind to (default="+str(DEFAULT_BIND[1])+")",default=DEFAULT_BIND[1]) 55 | parser.add_argument("--mfe","--maxfreqerror",dest="maxFreqError", type=int,action="store",default=DEFAULT_PPM,help="Set the maximum frequency error in ppm (default="+str(DEFAULT_PPM)+")") 56 | parser.add_argument("--fr", "--followup-replies", dest="followup", action="store_true", default=False, help="Configure server to send follow-up responses (default=not)") 57 | args = parser.parse_args() 58 | 59 | if args.quiet: 60 | logging.disable(logging.CRITICAL) 61 | else: 62 | logging.basicConfig(level=args.loglevel[0]) 63 | 64 | if not args.quiet: 65 | print "----" 66 | print "CSS-WC endpoint bound to host %s on port %d" % (args.wc_addr, args.wc_port) 67 | print "----" 68 | 69 | clock=SysClock(maxFreqErrorPpm=args.maxFreqError) 70 | wc_server=WallClockServer(clock, None, None, args.wc_addr, args.wc_port, followup=args.followup) 71 | wc_server.start() 72 | 73 | while True: 74 | time.sleep(1) 75 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- 1 | # dummy file to allow sphinxdoc to find this "module" 2 | -------------------------------------------------------------------------------- /examples/_useDvbCssUninstalled.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2015 British Broadcasting Corporation 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # allow examples to run from the package directory, if dvbcss isn't yet installed 18 | try: 19 | import dvbcss 20 | except ImportError: 21 | import sys, os 22 | parentDir= os.path.dirname(os.path.abspath(__file__))+os.sep+".." 23 | sys.path.append(parentDir) 24 | 25 | if __name__ == "__main__": 26 | import sys 27 | 28 | print >> sys.stderr, """ 29 | This is a support file and is designed to be imported, not run on its own. 30 | 31 | This module amends the import path to include the parent directory, 32 | thereby allowing the example code to run without installing dvbcss 33 | modules first. 34 | 35 | """ 36 | -------------------------------------------------------------------------------- /examples/clocksAndTasks.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2015 British Broadcasting Corporation 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | """\ 18 | .. py:module:: examples.clocksAndTasks 19 | 20 | This is an example showing how to use the :mod:`dvbcss.clock` and :mod:`dvbcss.task` modules. 21 | 22 | It creates a :class:`~dvbcss.clock.SysClock` and then other clocks use that as their parent clock. 23 | 24 | The task module functions are used to schedule callbacks when specific amounts of time have elapsed 25 | for particular clocks. 26 | 27 | Some of the clocks are adjusted (e.g. their tick rate or correlations are changed) on the fly, 28 | demonstrating that the scheduled callbacks are also adjusted to still happen at the right times 29 | (relative to the appropriate clock object). 30 | """ 31 | 32 | if __name__ == '__main__': 33 | 34 | import _useDvbCssUninstalled # Enable to run when dvbcss not yet installed ... @UnusedImport 35 | 36 | from dvbcss.clock import SysClock, TunableClock, CorrelatedClock, Correlation 37 | from dvbcss.task import sleepUntil, runAt 38 | 39 | import logging 40 | import threading 41 | import dvbcss.monotonic_time as time 42 | 43 | logging.basicConfig(level=logging.WARNING) 44 | 45 | sysClock = SysClock() 46 | tClock = TunableClock(sysClock, tickRate=100) 47 | 48 | for i in range(0, 2): 49 | print "Sleeping for 1 second according to sys clock" 50 | sleepUntil(sysClock, sysClock.ticks + sysClock.tickRate) 51 | print "Woken" 52 | for i in range(3, 5): 53 | print "Sleeping for 1 second according to tunable clock" 54 | sleepUntil(tClock, i * 100) 55 | print "Woken" 56 | 57 | def threadrun(): 58 | # separate thread in which we'll use sleepUntil on a clock that uses tClock as its parent 59 | cClock = CorrelatedClock(tClock, tickRate=10, correlation=Correlation(tClock.ticks, 0)) 60 | for i in range(0, 10): 61 | sleepUntil(cClock, i * 10) 62 | print "Tick", cClock.ticks 63 | 64 | print "Starting 1 tick per second in separate thread for 10 seconds" 65 | print "Will double clock speed halfway through" 66 | 67 | threading.Thread(target=threadrun).start() 68 | sleepUntil(tClock, tClock.ticks + 500) 69 | tClock.speed = 2 70 | print "Speed doubled" 71 | sleepUntil(tClock, tClock.ticks + 600) 72 | 73 | def go(message): 74 | print "Scheduled task ran. Message was: "+message 75 | 76 | print "Scheduling call back in 2 seconds..." 77 | args = ["huzzah!"] 78 | runAt(tClock, tClock.ticks + 400, go, args) 79 | 80 | time.sleep(5) 81 | 82 | print "Now same again, but with all callbacks scheduled in advance" 83 | tClock.slew = 0 84 | 85 | def doAdjust(amount): 86 | tClock.slew = amount 87 | print"Adjusted tClock frequency by +100 Hz" 88 | 89 | now=tClock.ticks 90 | for i in range(1,11): 91 | args = ["Tick "+str(100*i)] 92 | runAt(tClock, now+100*i, go, args) 93 | 94 | args=[100] 95 | runAt(tClock, now+500, doAdjust, args) 96 | 97 | time.sleep(11) 98 | 99 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | cherrypy>=10.0.0,<=11.0.0 2 | ws4py>=0.3.6 3 | six>=1.11.0 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2015 British Broadcasting Corporation 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from setuptools import setup 18 | import os 19 | import sys 20 | import re 21 | 22 | 23 | def find_packages(path, base="" ): 24 | """ Find all packages in path """ 25 | packages = {} 26 | if "__init__.py" in os.listdir(path): 27 | packages[base] = path 28 | 29 | for item in os.listdir(path): 30 | itempath = os.path.join(path,item) 31 | if os.path.isdir(itempath): 32 | newbase = "%s.%s" % (base, item) 33 | packages.update(find_packages(itempath, newbase)) 34 | 35 | return packages 36 | 37 | packages = find_packages("dvbcss","dvbcss") 38 | package_names = packages.keys() 39 | 40 | otherArgs = {} 41 | 42 | # if registering or uploading to PyPI: convert markdown readme to ReStructuredText 43 | # using pandoc 44 | 45 | lcase_args = [arg.lower() for arg in sys.argv] 46 | if "register" in lcase_args or "upload" in lcase_args: 47 | retval = os.system("pandoc --from=markdown --to=rst --output=tmp.README.rst README.md") 48 | if retval==0: 49 | otherArgs["long_description"] = open("tmp.README.rst").read() 50 | else: 51 | raise RuntimeError("Unable to convert documentation from Markdown to ReStructuredText. Is 'pandoc' command line tool installed?") 52 | 53 | try: 54 | VERSION, _ = re.match("^([.0-9a-zA-Z]+)-(.+)$", open("VERSION").read().replace("\n","").replace("\r","")).groups() 55 | 56 | setup( 57 | name = "pydvbcss", 58 | version = VERSION, 59 | author = "Matt Hammond (British Broadcasting Corporation)", 60 | author_email = "matt.hammond@bbc.co.uk", 61 | description = ("pydvbcss is a library implementing DVB \"CSS\" protocols for Companion Screen Synchronisation."), 62 | license = "Apache 2.0", 63 | keywords = "dvb companion synchronisation synchronization second-screen protocol", 64 | url = "http://github.com/BBC/pydvbcss", 65 | 66 | packages = package_names, 67 | package_dir = packages, 68 | install_requires = filter(len, [req.strip() for req in open("requirements.txt","r").read().splitlines()]), 69 | 70 | test_suite = "test.test_all.testSuite", 71 | 72 | classifiers=[ 73 | "Intended Audience :: Developers", 74 | "Intended Audience :: Telecommunications Industry", 75 | "License :: OSI Approved :: Apache Software License", 76 | "Natural Language :: English", 77 | "Operating System :: MacOS :: MacOS X", 78 | "Operating System :: Microsoft :: Windows", 79 | "Operating System :: POSIX :: Linux", 80 | "Programming Language :: Python :: 2.7", 81 | "Topic :: Internet", 82 | "Topic :: Software Development :: Libraries :: Python Modules", 83 | "Topic :: System :: Networking :: Time Synchronization", 84 | ], 85 | 86 | **otherArgs 87 | ) 88 | 89 | finally: 90 | if "long_description" in otherArgs: 91 | os.remove("tmp.README.rst") 92 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- 1 | # this file exists to allow tests directory to be imported as a module by setuptools 2 | -------------------------------------------------------------------------------- /test/_useDvbCssUninstalled.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2015 British Broadcasting Corporation 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | # prioritise using dvbcss from here, not any instance that might already be installed 19 | import sys, os 20 | parentDir= os.path.dirname(os.path.abspath(__file__))+os.sep+".." 21 | sys.path.insert(0,parentDir) 22 | 23 | if __name__ == "__main__": 24 | import sys 25 | 26 | print >> sys.stderr, """ 27 | This is a support file and is designed to be imported, not run on its own. 28 | 29 | This module amends the import path to include the parent directory, 30 | thereby ensuring that dvbcss from this package will be used, instead of any installed 31 | instance. 32 | """ 33 | -------------------------------------------------------------------------------- /test/mock_dependent.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2015 British Broadcasting Corporation 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | class MockDependent(object): 18 | def __init__(self): 19 | super(MockDependent,self).__init__() 20 | self.notifications = [] 21 | def assertNotNotified(self, *args, **kwargs): 22 | if self.notifications: 23 | raise AssertionError("Observed notifications "+str(self.notifications)+" should have been []", *args, **kwargs) 24 | def notify(self,cause): 25 | self.notifications.append(cause) 26 | def assertNotificationsEqual(self, causes, *args, **kwargs): 27 | if self.notifications != causes: 28 | raise AssertionError("Observed notifications "+str(self.notifications)+" not equal to asserted "+str(causes), *args, **kwargs) 29 | self.notifications = [] 30 | 31 | -------------------------------------------------------------------------------- /test/test_Task.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2015 British Broadcasting Corporation 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import unittest 18 | 19 | import _useDvbCssUninstalled # Enable to run when dvbcss not yet installed ... @UnusedImport 20 | 21 | class Test(unittest.TestCase): 22 | 23 | 24 | def testName(self): 25 | pass 26 | 27 | 28 | if __name__ == "__main__": 29 | #import sys;sys.argv = ['', 'Test.testName'] 30 | unittest.main() 31 | -------------------------------------------------------------------------------- /test/test_all.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2015 British Broadcasting Corporation 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import unittest 18 | import os 19 | 20 | thisDir= os.path.dirname(os.path.abspath(__file__)) 21 | 22 | loader = unittest.TestLoader() 23 | testSuite = loader.discover(thisDir, pattern="test_*.py") 24 | 25 | if __name__ == "__main__": 26 | 27 | unittest.TextTestRunner().run(testSuite) 28 | -------------------------------------------------------------------------------- /test/test_monotonic_time.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2015 British Broadcasting Corporation 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import unittest 18 | 19 | import _useDvbCssUninstalled # Enable to run when dvbcss not yet installed ... @UnusedImport 20 | 21 | 22 | class Test_monotonic_time(unittest.TestCase): 23 | 24 | def setUp(self): 25 | import dvbcss.monotonic_time as m 26 | import time as t 27 | self.m=m 28 | self.t=t 29 | 30 | 31 | def test_time_increments(self): 32 | """Check time flows at roughly the right rate""" 33 | t=self.t 34 | m=self.m 35 | 36 | start_t = t.time() 37 | start_m = m.time() 38 | t.sleep(5) 39 | end_t = t.time() 40 | end_m = m.time() 41 | 42 | diff_t = (end_t - start_t) 43 | diff_m = (end_m - start_m) 44 | 45 | diff = diff_t - diff_m 46 | 47 | self.assertLess(abs(diff), 0.05, "Time is within 1% between monotonic_time and time") 48 | 49 | 50 | def test_sleep_works(self): 51 | """Check a sleep works""" 52 | t=self.t 53 | m=self.m 54 | 55 | start_t = t.time() 56 | m.sleep(5) 57 | end_t = t.time() 58 | 59 | diff_t = (end_t - start_t) 60 | 61 | self.assertLess(abs(5.0-diff_t), 0.05, "Sleep was correct to within 1%") 62 | 63 | if __name__ == "__main__": 64 | unittest.main() 65 | -------------------------------------------------------------------------------- /test/test_wc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2015 British Broadcasting Corporation 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import unittest 18 | 19 | import _useDvbCssUninstalled # Enable to run when dvbcss not yet installed ... @UnusedImport 20 | 21 | from dvbcss.protocol.wc import WCMessage as WCMessage 22 | 23 | class Test(unittest.TestCase): 24 | 25 | 26 | def testSmokeTestCreate(self): 27 | m=WCMessage(WCMessage.TYPE_REQUEST, 1, 256, 2, 3, 4) 28 | self.assertEqual(m.msgtype, WCMessage.TYPE_REQUEST) 29 | self.assertEqual(m.precision, 1) 30 | self.assertEqual(m.maxFreqError, 256) 31 | self.assertEqual(m.originateNanos, 2) 32 | self.assertEqual(m.receiveNanos, 3) 33 | self.assertEqual(m.transmitNanos, 4) 34 | self.assertEqual(m.originalOriginate, None) 35 | 36 | def test_simplePayload(self): 37 | m=WCMessage(WCMessage.TYPE_RESPONSE, 5, 7680, 1000, 2000, 3000) 38 | payload=m.pack() 39 | self.assertEqual(payload, "\x00\x01\x05\x00\x00\x00\x1e\x00\x00\x00\x00\x00\x00\x00\x03\xe8\x00\x00\x00\x00\x00\x00\x07\xd0\x00\x00\x00\x00\x00\x00\x0b\xb8") 40 | 41 | def test_simplePayloadOverridingOriginate(self): 42 | m=WCMessage(WCMessage.TYPE_RESPONSE, 5, 12160, 1000, 2000, 3000, (0xaabbccdd, 0xeeff1122)) 43 | payload=m.pack() 44 | self.assertEqual(payload, "\x00\x01\x05\x00\x00\x00\x2f\x80\xaa\xbb\xcc\xdd\xee\xff\x11\x22\x00\x00\x00\x00\x00\x00\x07\xd0\x00\x00\x00\x00\x00\x00\x0b\xb8") 45 | 46 | def test_simpleParseWithUnusualOriginate(self): 47 | payload = "\x00\x01\x05\x00\x00\x01\xf4\x00\xaa\xbb\xcc\xdd\xee\xff\x11\x22\x00\x00\x00\x00\x00\x00\x07\xd0\x00\x00\x00\x00\x00\x00\x0b\xb8" 48 | m=WCMessage.unpack(payload) 49 | self.assertEquals(m.msgtype, WCMessage.TYPE_RESPONSE) 50 | self.assertEquals(m.precision, 5) 51 | self.assertEquals(m.maxFreqError, 500*256) 52 | self.assertEquals(m.originalOriginate, (0xaabbccdd, 0xeeff1122)) 53 | self.assertEquals(m.receiveNanos, 2000) 54 | self.assertEquals(m.transmitNanos, 3000) 55 | 56 | def test_simpleParse(self): 57 | payload = "\x00\x01\x05\x00\x00\x01\xf4\x00\xaa\xbb\xcc\xdd\x3b\x9a\xc9\xff\x00\x00\x00\x00\x00\x00\x07\xd0\x00\x00\x00\x00\x00\x00\x0b\xb8" 58 | m=WCMessage.unpack(payload) 59 | self.assertEquals(m.msgtype, WCMessage.TYPE_RESPONSE) 60 | self.assertEquals(m.precision, 5) 61 | self.assertEquals(m.maxFreqError, 500*256) 62 | self.assertEquals(m.originateNanos, 2864434397999999999) 63 | self.assertEquals(m.receiveNanos, 2000) 64 | self.assertEquals(m.transmitNanos, 3000) 65 | 66 | def test_encodePrecision(self): 67 | self.assertEquals(WCMessage.encodePrecision(2**-128), -128) 68 | self.assertEquals(WCMessage.encodePrecision(0.00001), -16 ) 69 | self.assertEquals(WCMessage.encodePrecision(2**127), 127) 70 | self.assertEquals(WCMessage.encodePrecision(0.0007), -10 ) 71 | self.assertEquals(WCMessage.encodePrecision(0.001), -9 ) 72 | 73 | def test_encodeMaxFreqError(self): 74 | self.assertEquals(WCMessage.encodeMaxFreqError(50), 12800) 75 | self.assertEquals(WCMessage.encodeMaxFreqError(1900), 486400) 76 | self.assertEquals(WCMessage.encodeMaxFreqError(0.01), 3) 77 | self.assertEquals(WCMessage.encodeMaxFreqError(28), 7168) 78 | self.assertEquals(WCMessage.encodeMaxFreqError(100000), 25600000) 79 | self.assertEquals(WCMessage.encodeMaxFreqError(0), 0) 80 | 81 | def test_decodePrecision(self): 82 | self.assertEquals(WCMessage.decodePrecision(-128), 2**-128) 83 | self.assertEquals(WCMessage.decodePrecision(-16), 2**-16 ) 84 | self.assertEquals(WCMessage.decodePrecision(127), 2**127 ) 85 | self.assertEquals(WCMessage.decodePrecision(-10), 2**-10 ) 86 | self.assertEquals(WCMessage.decodePrecision(-9), 2**-9 ) 87 | 88 | def test_decodeMaxFreqError(self): 89 | self.assertEquals(WCMessage.decodeMaxFreqError(12800), 50 ) 90 | self.assertEquals(WCMessage.decodeMaxFreqError(486400), 1900 ) 91 | self.assertEquals(WCMessage.decodeMaxFreqError(3), 0.01171875) 92 | self.assertEquals(WCMessage.decodeMaxFreqError(7168), 28 ) 93 | self.assertEquals(WCMessage.decodeMaxFreqError(25600000), 100000 ) 94 | self.assertEquals(WCMessage.decodeMaxFreqError(0), 0 ) 95 | 96 | if __name__ == "__main__": 97 | #import sys;sys.argv = ['', 'Test.testSmokeTestCreate'] 98 | unittest.main() 99 | --------------------------------------------------------------------------------