Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

- Initial import.

  • Loading branch information...
commit f4d1ffe7f7b9140e1073e649002ba0acd928bd06 0 parents
Ludo Antonov authored
Showing with 8,948 additions and 0 deletions.
  1. +29 −0 .gitignore
  2. +36 −0 INSTALL
  3. +244 −0 README.md
  4. +83 −0 bin/dripls
  5. +30 −0 bin/set_ts_lo.sh
  6. +29 −0 dripls/.gitignore
  7. 0  dripls/__init__.py
  8. +1 −0  dripls/conf/__init__.py
  9. +42 −0 dripls/conf/common.py
  10. +81 −0 dripls/conf/data.py
  11. +1 −0  dripls/conf/dev.py
  12. +1 −0  dripls/conf/local.py
  13. 0  dripls/deploy/__init__.py
  14. +111 −0 dripls/deploy/fabfile.py
  15. +1 −0  dripls/fabfile.py
  16. +95 −0 dripls/httpls_client.py
  17. +146 −0 dripls/main.py
  18. +257 −0 dripls/shaper.py
  19. +4 −0 dripls/test/__init__.py
  20. +21 −0 dripls/test/common_conf_tests.py
  21. +75 −0 dripls/test/httpls_tests.py
  22. +1 −0  dripls/test/nose_with_coverage.sh
  23. +131 −0 dripls/test/shape_tests.py
  24. +71 −0 dripls/test/web_tests.py
  25. 0  dripls/test/wt_suite/basic/mock_segment.ts
  26. +32 −0 dripls/test/wt_suite/basic/wt_1000k.m3u8
  27. +36 −0 dripls/test/wt_suite/basic/wt_1000k_asset_ad.m3u8
  28. +32 −0 dripls/test/wt_suite/basic/wt_1000k_with_cdn.m3u8
  29. +12 −0 dripls/test/wt_suite/basic/wt_master_cdn_fallback.m3u8
  30. +12 −0 dripls/test/wt_suite/local/wt.m3u8
  31. +28 −0 dripls/test/wt_suite/local/wt_1000k.m3u8
  32. +28 −0 dripls/test/wt_suite/local/wt_1700k.m3u8
  33. +28 −0 dripls/test/wt_suite/local/wt_250k.m3u8
  34. +28 −0 dripls/test/wt_suite/local/wt_4000k.m3u8
  35. +28 −0 dripls/test/wt_suite/local/wt_650k.m3u8
  36. BIN  dripls/test/wt_suite/segments/wt_1000k_final_0000.ts
  37. BIN  dripls/test/wt_suite/segments/wt_1000k_final_0001.ts
  38. BIN  dripls/test/wt_suite/segments/wt_1000k_final_0002.ts
  39. BIN  dripls/test/wt_suite/segments/wt_1000k_final_0003.ts
  40. BIN  dripls/test/wt_suite/segments/wt_1000k_final_0004.ts
  41. BIN  dripls/test/wt_suite/segments/wt_1000k_final_0005.ts
  42. BIN  dripls/test/wt_suite/segments/wt_1000k_final_0006.ts
  43. BIN  dripls/test/wt_suite/segments/wt_1000k_final_0007.ts
  44. BIN  dripls/test/wt_suite/segments/wt_1000k_final_0008.ts
  45. BIN  dripls/test/wt_suite/segments/wt_1000k_final_0009.ts
  46. BIN  dripls/test/wt_suite/segments/wt_1000k_final_0010.ts
  47. BIN  dripls/test/wt_suite/segments/wt_1000k_final_0011.ts
  48. BIN  dripls/test/wt_suite/segments/wt_1700k_final_0000.ts
  49. BIN  dripls/test/wt_suite/segments/wt_1700k_final_0001.ts
  50. BIN  dripls/test/wt_suite/segments/wt_1700k_final_0002.ts
  51. BIN  dripls/test/wt_suite/segments/wt_1700k_final_0003.ts
  52. BIN  dripls/test/wt_suite/segments/wt_1700k_final_0004.ts
  53. BIN  dripls/test/wt_suite/segments/wt_1700k_final_0005.ts
  54. BIN  dripls/test/wt_suite/segments/wt_1700k_final_0006.ts
  55. BIN  dripls/test/wt_suite/segments/wt_1700k_final_0007.ts
  56. BIN  dripls/test/wt_suite/segments/wt_1700k_final_0008.ts
  57. BIN  dripls/test/wt_suite/segments/wt_1700k_final_0009.ts
  58. BIN  dripls/test/wt_suite/segments/wt_1700k_final_0010.ts
  59. BIN  dripls/test/wt_suite/segments/wt_1700k_final_0011.ts
  60. BIN  dripls/test/wt_suite/segments/wt_250k_final_0000.ts
  61. BIN  dripls/test/wt_suite/segments/wt_250k_final_0001.ts
  62. BIN  dripls/test/wt_suite/segments/wt_250k_final_0002.ts
  63. BIN  dripls/test/wt_suite/segments/wt_250k_final_0003.ts
  64. BIN  dripls/test/wt_suite/segments/wt_250k_final_0004.ts
  65. BIN  dripls/test/wt_suite/segments/wt_250k_final_0005.ts
  66. BIN  dripls/test/wt_suite/segments/wt_250k_final_0006.ts
  67. BIN  dripls/test/wt_suite/segments/wt_250k_final_0007.ts
  68. BIN  dripls/test/wt_suite/segments/wt_250k_final_0008.ts
  69. BIN  dripls/test/wt_suite/segments/wt_250k_final_0009.ts
  70. BIN  dripls/test/wt_suite/segments/wt_250k_final_0010.ts
  71. BIN  dripls/test/wt_suite/segments/wt_250k_final_0011.ts
  72. BIN  dripls/test/wt_suite/segments/wt_4000k_final_0000.ts
  73. BIN  dripls/test/wt_suite/segments/wt_4000k_final_0001.ts
  74. BIN  dripls/test/wt_suite/segments/wt_4000k_final_0002.ts
  75. BIN  dripls/test/wt_suite/segments/wt_4000k_final_0003.ts
  76. BIN  dripls/test/wt_suite/segments/wt_4000k_final_0004.ts
  77. BIN  dripls/test/wt_suite/segments/wt_4000k_final_0005.ts
  78. BIN  dripls/test/wt_suite/segments/wt_4000k_final_0006.ts
  79. BIN  dripls/test/wt_suite/segments/wt_4000k_final_0007.ts
  80. BIN  dripls/test/wt_suite/segments/wt_4000k_final_0008.ts
  81. BIN  dripls/test/wt_suite/segments/wt_4000k_final_0009.ts
  82. BIN  dripls/test/wt_suite/segments/wt_4000k_final_0010.ts
  83. BIN  dripls/test/wt_suite/segments/wt_4000k_final_0011.ts
  84. BIN  dripls/test/wt_suite/segments/wt_650k_final_0000.ts
  85. BIN  dripls/test/wt_suite/segments/wt_650k_final_0001.ts
  86. BIN  dripls/test/wt_suite/segments/wt_650k_final_0002.ts
  87. BIN  dripls/test/wt_suite/segments/wt_650k_final_0003.ts
  88. BIN  dripls/test/wt_suite/segments/wt_650k_final_0004.ts
  89. BIN  dripls/test/wt_suite/segments/wt_650k_final_0005.ts
  90. BIN  dripls/test/wt_suite/segments/wt_650k_final_0006.ts
  91. BIN  dripls/test/wt_suite/segments/wt_650k_final_0007.ts
  92. BIN  dripls/test/wt_suite/segments/wt_650k_final_0008.ts
  93. BIN  dripls/test/wt_suite/segments/wt_650k_final_0009.ts
  94. BIN  dripls/test/wt_suite/segments/wt_650k_final_0010.ts
  95. BIN  dripls/test/wt_suite/segments/wt_650k_final_0011.ts
  96. +12 −0 dripls/test/wt_suite/wt_dripls/wt.m3u8
  97. +28 −0 dripls/test/wt_suite/wt_dripls/wt_1000k.m3u8
  98. +28 −0 dripls/test/wt_suite/wt_dripls/wt_1700k.m3u8
  99. +28 −0 dripls/test/wt_suite/wt_dripls/wt_250k.m3u8
  100. +28 −0 dripls/test/wt_suite/wt_dripls/wt_4000k.m3u8
  101. +28 −0 dripls/test/wt_suite/wt_dripls/wt_650k.m3u8
  102. +1 −0  fabfile.py
  103. +26 −0 nginx/default
  104. +6,000 −0 nginx/default-locations
  105. +1,015 −0 nginx/proxy
29 .gitignore
@@ -0,0 +1,29 @@
+.project
+.pydevproject
+
+*.py[co]
+*.log
+
+# Packages
+*.egg
+*.egg-info
+dist
+build
+eggs
+parts
+develop-eggs
+.installed.cfg
+
+
+tags
+
+# Installer logs
+pip-log.txt
+
+# Unit test / coverage reports
+.coverage
+.tox
+lint.xml
+
+
+test/playlists/*
36 INSTALL
@@ -0,0 +1,36 @@
+DripLS
+
+Prerequisites
+==================
+
+Currently DripLS runs on linux based systems, as it makes use of iptables/netem.
+In future it is possible to enable DripLS to run on BSD based distros via ipfw.
+
+Basic Installation
+==================
+
+To setup DripLS :
+
+1. Make sure iptables is available and kernel is compiled with netem support.
+ This should be the default on most linux systems.
+
+2. Make sure the user running the service has sudo rights
+
+3. Install nginx
+
+4. Copy the configuration sites for nginx from conf/nginx/. Make sure to include
+ them in the nginx config.
+
+5. Execute fab local deploy so that DripLS can setup its components, or make a new
+ target in the deploy/fabfile for the remote server on which to deploy Dripls
+
+6. ps aux | grep dripls on the target server to verify the app is running
+
+7. curl "http://<dripls-host>/master.m3u8?authkey=sample&cid=wt&r=650k.s0~e404" and
+ make sure there is a valid response
+
+Custom Adapter
+==================
+To extend the DripLS functionally to handle specifics of your HTL server, extend
+HttplsProvider in conf/data.py
+
244 README.md
@@ -0,0 +1,244 @@
+Summary
+==========
+
+DripLS - Make a CDN in a box service that is able to perform traffic shaping for testing purposes on a http live stream.
+
+Usage
+==========
+
+ http://dripls-host/cache?authkey=[authkey]&cid=[cid]&tag=[tag]&r=[rules]
+
+Prepare a traffic shaped playlist and return its streamable url on completion along with segments which were rule matched (along with their matched url). The cache request is synchronous. The call will return when the matching segments and vplaylists are cached and traffic shaped. Use /cache, to avoid re-generating the same playlist with the same rules over and over again.
+
+ http://dripls-host/master.m3u8?authkey=[authkey]&cid=[cid]&tag=[tag]&r=[rules]
+
+Returns back an actual streamable m3u8 playlist. master.m3u8 calls cache internally, so this call is inherently synchronous. It will return the master playlist as soon as the playlist traffic shaping is done. This is useful for feeding this url directly to m3u8 players to achieve the effect of on-the-fly traffic shaping, while still serving a playable playlist back. For example directly feeding a master.m3u8 to a device player, should still result in the device playing the video stream properly.
+
+ http://dripls-host/tag.m3u8?authkey=[authkey]&tag=[tag]
+
+Returns back a streamable m3u8 playlist, previously prepared via a call to /master.m3u8 or /cache with a tag argument. When the call to master.m3u8 or cache was made, all url arguments(except authkey) have gotten stored locally under tag tag. tag.m3u8 uses the previously specified query arguments in the current shaping call. The main purpose of tag.m3u8 is to reduce the complexity and provide aliasing for complex rule sets. Additional arguments, such as cid, r, can be specified to override the arguments previously specified.
+
+ http://dripls-host/updatesegment?authkey=[authkey]&url=[previously_shaped_url|previously_shaped_url]&new_action=[rule_action|rule_action]
+
+Re-shapes a segment, specified by previously\_shaped\_url. The previously\_shaped\_url is retrieved via call to /cache, which performs the initial shape. The new_action is the action portion of a rule ( ex. net100.loss10) . Note that the segment MUST be previously shaped with a net rule in order to be reshaped. Segments that are matched by the e(rror) rule cannot be re-shaped, due to the fact that a playlist structure change must occur, which could lead to the possibility of inconsistent results due to client content caching. Re-shaping via /updatesegment is virtually transparent to the client, and can occur in-flight during streaming. During re-shaping only the actual segment shaping rules (on the server) are changed, while the actual playlist is not modified.
+
+Parameter Details
+==========
+
+
+__authkey__ - authentication key to protect the service from distributing content to unauthorized users
+
+__cid__ - content id ( default package supports cid=wt , which is test video)
+
+__tag__ - can be specified while calling /master.m3u8 or /cache to allow later re-invocation of the rules + cid url via /tag.m3u8
+
+__r__ - rules to transform the httpls m3u8 files, comma separated, format rule~action. For more detailed information see the Rules section
+
+
+Rule Format
+==========
+
+Each rule has the following format: rule-match-expresison~action.
+
+Rule Match Expression
+===============
+
+Each rule expression is in the format optional cdn.bitrate.optional segment. All parts of the expression support wild-cards. When a rule expression matches a segment, then the action associated with the expression is applied to the segment itself.
+
+For example, 650k.s0 as a rule match expression would match the first segment of the 650kbit playlist and apply the action part to that segment. Similarly, .s0 would apply the action for the first segment of all variable playlists, and v.650k.s would match all segments in the 650kbit playlist for cdn V.
+
+If the segment part of the expression is missing, the assumption is that the rule action is to be applied to the variant playlist, as opposed to its segment(s). For example a rule like 650k~e404, would translate to the master m3u8 playlist having a 404 link for its 650kbit playlist.
+
+Here are the available options for each rule expression:
+
+CDN
+===============
+
+
+ __cdn__ - specifies the cdn from which the serving is happening. The cdn is extracted from the url and can be arbitrarily configured as needed. Current Hulu CDNs are limited to : a,v ( ex v.650k~e404, would translate to the 650kbit playlist on the v cdn having all of its segments replaced with 404 page references)
+
+ __*none*__ - cdn can be ommited in the expression, which would be interpreted as meaning from "any" cdn
+
+Bitrate
+===============
+
+
+__[bitrate]k__ - specifies the bit rate of the playlist for which the rule expression is for (ex. 650k , 1000k, 1500k)
+
+__\*__ - wild-card can be used to mean "any" bit-rate playlist
+
+Segment
+===============
+__s[number]__ - Specifies the absolute(regardless of type) segment number within the variable
+ playlist for which the rule expression is for (ex. s0 - first segment, s1 -
+ second segm ent, s2 - third segment)
+
+__s\*__ - Wild-card can be used to mean "any" segment within the playlist
+
+__c[number]__ - Specifies the content ( not ad or pre-roll) segment number within the variable
+ playlist for which the rule expression is for (ex. c0 - first content segment after ads/pre-rolls)
+
+__c\*__ - Wild-card can be used to mean "any" content( not ad or pre-roll ) segment within the playlist
+
+__a[number]__ - Specifies the ad segment number within the variable playlist for which the rule expression
+ is for (ex. a0 - first ad segment)
+
+__a*__ - Wild-card can be used to mean "any" ad segment within the playlist
+
+
+Rule Action
+==============
+This is the action to be applied to a segment or playlist, when a matching rule is found for it:
+
+Action
+==================
+__e[http error code]__ - Replace the playlist's matching segment url with a url that returns a 404 status code upon invocation ( ex. e404 )
+
+__net[bandwidth in kbps]loss[% of packets dropped]__ - The net rule action, when applied to a segment, causes the segment to be locally cached and then served at
+ [bandwidth in kbps] with [% of packets dropped] ( ex. net200loss10 - serve the matched segment at 200kbps
+ max with 10% packet loss during transmission)
+
+
+Master Playlist Fetching and Options
+==================
+
+Content fetching is piped to the original m3u8 provider. As such the original provider might request or offer additional optional functionality.
+
+Local (wt)
+======================
+The local test ( world travel) video does not offer additional features at that time.
+
+Custom
+======================
+Custom url parameters can be passed in the url line, and they will be forwarded to the original m3u8 server for correct fetching of the content. Examples can include specifying multiple cdns, additional authentication parameters, device ids , etc.
+
+Auth Key
+======================
+use authkey=sample for cid=wt ( for world travel local sample video)
+
+authkey is not required, but is recommended to provide simple protection to the service
+
+Some Examples
+======================
+Replace dripls-host with the appropriate dripls instance
+
+Variant playlist has first segment that 404s
+
+ http://dripls-host/master.m3u8?authkey=sample&cid=wt&r=650k.s0~e404
+
+>Fail first segment on 650k playlist (usually an asset segment, switch to c0 for first content segment) with 404
+
+
+Variant playlist has middle segment that 404s
+
+ http://dripls-host/master.m3u8?authkey=sample&cid=wt&r=650k.s20~e404
+
+>Fail segment 20 on 650k playlist with 404
+
+
+Variant playlist has several consecutive segments that 404
+
+ http://dripls-host/master.m3u8?authkey=sample&cid=wt&r=650k.s1~e404,650k.s2~e404,650k.s3~e404,650k.s4~e404
+
+>Fail segments 1-4 on the 650k playlist with 404
+
+
+Master playlist has variant playlist that 404s
+
+ http://dripls-host/master.m3u8?authkey=sample&cid=wt&r=650k~e404
+
+>650k playlist 404s in master playlist
+
+
+Multiple variant playlists 404 on the same segment
+
+ http://dripls-host/master.m3u8?authkey=sample&cid=wt&r=*.s0~e404
+
+>Fail first segment on all variant playlists
+
+ http://dripls-host/master.m3u8?authkey=sample&cid=wt&r=650k.s0~e404,1000k.s0~e500
+
+>Fail first segment 404 on 650k and 500 on the 1000k variant playlist
+
+ http://dripls-host/master.m3u8?authkey=sample&cid=wt&r=*.s12~e404,*.s13~e404
+
+>Fail two consecutive segments on all variant playlists
+
+ http://dripls-host/master.m3u8?authkey=sample&cid=wt&r=*.c0~e404
+
+>Fail first content segment (after assets/pre-roll ads)
+
+
+Traffic shape segments
+
+ http://dripls-host/master.m3u8?authkey=sample&cid=wt&r=*.c1~net5
+
+>Cache the second content segment of each playlist, and rewrite the vplaylist to serve the segment with maximum download speed of 5kbs
+
+ http://dripls-host/master.m3u8?authkey=sample&cid=wt&r=*.c2~net500.loss10
+
+>Cache the third content segment of each playlist, and rewrite the vplaylist to serve the segment with maximum download speed of 500kbs and 10% packet loss
+
+ http://dripls-host/master.m3u8?authkey=sample&cid=wt&r=*.c1~net5,*.c2~net500.loss10
+
+>Combine the above two traffic shaping rules
+
+
+CDN Specific targeting
+
+ http://dripls-host/master.m3u8?authkey=sample&cid=wt&r=a.650k.s*~e404
+>For the 650kbit playlist coming from CDN 'a' , transform all segment urls to 404 error message urls
+
+ http://dripls-host/master.m3u8?authkey=sample&cid=wt&r=a.*.s1~e404
+>For the second segment in all variant playlists coming from CDN 'a' , transform their urls to to 404 error messages
+
+
+Some Cache/Reshape Examples
+======================
+
+Initial shape
+
+ http://dripls-host/cache?authkey=sample&cid=wt&r=4000k.s2~net10
+
+>Perform the initial shape, streaming s2 at 10kbs . Result would return small JSON object for example:
+>Output: { "4000000._d.s2": "http://dripls-host/s/10001/ts.ts?s=e92cc3a7270d48c34398ad894a83d34fb23d9741a01cc16c20290bb8",
+> "id": "wt_bfb7c6b6d16141128545aed48bddc93a", "url": "http://dripls-host/playlist.m3u8?p=m_wt_bfb7c6b6d16141128545aed48bddc93a" }
+
+>The 4000k segment 2 has been shaped to : http://dripls-host/s/10001/ts.ts?s=e92cc3a7270d48c34398ad894a83d34fb23d9741a01cc16c20290bb8
+
+Re-shape
+
+ http://dripls-host/updatesegment?authkey=sample&url=http://dripls-host/s/10001/ts.ts?s=e92cc3a7270d48c34398ad894a83d34fb23d9741a01cc16c20290bb8&new_action=net1000
+
+>Now the segment s2 would continue to be streamed at 1000kbs
+
+License
+==========
+Copyright (C) 2010-2011 by Hulu, LLC
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+Contact
+==========
+
+For support or for further questions, please contact:
+
+ dripls-dev@googlegroups.com
+
+
83 bin/dripls
@@ -0,0 +1,83 @@
+#!/usr/bin/env python2.7
+
+import argparse
+import os
+import sys
+import traceback
+import logging
+import cherrypy
+import setproctitle
+
+sys.path.insert(0, os.path.realpath(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'dripls')))
+
+import conf as config
+import main
+
+def _daemonize():
+ cherrypy.config.update({ 'log.screen' : False })
+ if hasattr(cherrypy.engine, 'signal_handler'):
+ cherrypy.engine.signal_handler.subscribe()
+ if hasattr(cherrypy.engine, 'console_control_handler'):
+ cherrypy.engine.console_control_handler.subscribe()
+ daemon = cherrypy.process.plugins.Daemonizer(cherrypy.engine)
+ daemon.subscribe()
+
+if __name__ == '__main__':
+ try:
+ parser = argparse.ArgumentParser(description='Hulu Plus feed service.')
+ parser.add_argument('-b', '--bind', dest='bind_address',
+ help='Socket to bind')
+ parser.add_argument('--daemon', action='store_true',
+ help='Daemonize process')
+ parser.add_argument('-p', '--pid', dest='pidfile',
+ help='Filename for the PID file')
+ parser.add_argument('--port', dest='port', type=int,
+ help='Port to listen on')
+ parser.add_argument('--app_root_url', dest='app_root_url',
+ help='External url in case the app is behind a proxy')
+ parser.add_argument('--access-log', dest='access_log',
+ help='Path for access log')
+ parser.add_argument('--error-log', dest='error_log',
+ help='Path for error log')
+ args = parser.parse_args()
+
+ if args.bind_address:
+ config.socket = args.bind_address
+ if args.pidfile:
+ config.pidfile = args.pidfile
+ if args.port:
+ config.port = args.port
+ if args.access_log:
+ config.access_file = args.access_log
+ if args.error_log:
+ config.error_file = args.error_log
+ if args.app_root_url:
+ config.app['root_url'] = args.app_root_url
+
+ cherrypy.engine.subscribe('start', main.root.on_server_start)
+ cherrypy.engine.subscribe('start', main.root.on_start)
+
+ cherrypy.config.update({
+ 'log.screen' : True,
+ 'tools.gzip.on' : True,
+ 'server.socket_host' : config.socket,
+ 'server.socket_port' : config.port,
+ 'server.thread_pool' : config.thread_pool,
+ 'server.socket_timeout' : 30
+ })
+
+ if args.daemon:
+ _daemonize()
+
+ if config.pidfile:
+ cherrypy.process.plugins.PIDFile(cherrypy.engine, config.pidfile).subscribe()
+
+ setproctitle.setproctitle(os.path.basename(__file__))
+ cherrypy.engine.start()
+ cherrypy.engine.block()
+ except Exception, e:
+ logging.error("Server failed to start. Exiting ...")
+ logging.error(e)
+ logging.error(traceback.print_exc(file=sys.stdout))
+ sys.exit(1)
+
30 bin/set_ts_lo.sh
@@ -0,0 +1,30 @@
+#!/bin/bash
+
+if [ $# -ne 3 ]
+then
+ echo "Usage: `basename $0` port <speed-limit-in-kbps] [packet-loss]"
+ exit $E_BADARGS
+fi
+
+hexport=$(echo "obase=16; $1" | bc)
+netem_loss_handle="$12"
+
+# Add main classes
+/sbin/tc qdisc add dev lo root handle 1: htb
+/sbin/tc class add dev lo parent 1: classid 1:1 htb rate 1000000kbps
+
+
+echo "------- Remove any previous rule"
+# Delete any old rules (if rules are missing , failure in these commands can be expected)
+/sbin/tc qdisc del dev lo parent 1:$hexport handle $netem_loss_handle
+/sbin/tc filter del dev lo parent 1:0 prio $1 protocol ip handle $1 fw flowid 1:$hexport
+/sbin/tc class del dev lo parent 1:1 classid 1:$hexport
+/sbin/iptables -D OUTPUT -t mangle -p tcp --sport $1 -j MARK --set-mark $1
+
+echo "------- Adding rule"
+# Add the new rule
+/sbin/tc class add dev lo parent 1:1 classid 1:$hexport htb rate $2kbps ceil $2kbps prio $1
+/sbin/tc filter add dev lo parent 1:0 prio $1 protocol ip handle $1 fw flowid 1:$hexport
+/sbin/tc qdisc add dev lo parent 1:$hexport handle $netem_loss_handle: netem loss $3%
+/sbin/iptables -A OUTPUT -t mangle -p tcp --sport $1 -j MARK --set-mark $1
+
29 dripls/.gitignore
@@ -0,0 +1,29 @@
+.project
+.pydevproject
+
+*.py[co]
+*.log
+
+# Packages
+*.egg
+*.egg-info
+dist
+build
+eggs
+parts
+develop-eggs
+.installed.cfg
+
+
+tags
+
+# Installer logs
+pip-log.txt
+
+# Unit test / coverage reports
+.coverage
+.tox
+lint.xml
+
+
+test/playlists/*
0  dripls/__init__.py
No changes.
1  dripls/conf/__init__.py
@@ -0,0 +1 @@
+from common import *
42 dripls/conf/common.py
@@ -0,0 +1,42 @@
+import cherrypy
+import urlparse
+import uuid
+import os
+
+# Service
+socket = '0.0.0.0'
+dripls_main_site_port = 8080
+thread_pool = 10
+
+bin_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))), "bin")
+pidfile = os.path.join(bin_path, "dripls.pid")
+error_log = os.path.join(bin_path, "error_log.log")
+
+app = {
+ 'root_url': cherrypy.url()
+}
+
+# Shaper path
+shaper_path = os.path.join(bin_path, "set_ts_lo.sh")
+
+# Shape port range
+shape_start_port = 10000
+shape_end_port = 11000
+
+# Environment overrides
+if os.path.exists(os.path.join(os.path.dirname(os.path.realpath(__file__)), "env.py")):
+ from env import *
+else:
+ from local import *
+
+port = int(dripls_main_site_port)
+
+# Final url rewrite. Hack to battle the fact that cherrypy is behind a proxy on different port
+def get_final_url(path, args):
+ cherrypy_url = cherrypy.url(path, args)
+
+ scheme, netloc, path, qs, anchor = urlparse.urlsplit(cherrypy_url)
+ return urlparse.urlunsplit( (scheme, urlparse.urlsplit(app['root_url'])[1], path, qs, anchor))
+
+def get_seeded_cid(cid):
+ return "{0}_{1}".format(cid, uuid.uuid4().hex)
81 dripls/conf/data.py
@@ -0,0 +1,81 @@
+"""
+ Base data provider for customizable options that allow for extending the way that dripls fetches and shapes playlists
+"""
+
+import cherrypy
+import urlparse
+import hashlib
+import uuid
+import re
+import os
+import urllib2
+
+import common
+
+
+class HttplsProvider( object ):
+ """To provide custom httpls provider logic, inherit this class"""
+
+ base_path = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
+
+ def get_cdn_from_playlist_url(self, url):
+ """Seeds the cid in a format that is acceptable for debugging purposes"""
+
+ # overwrite with appropriate extraction method, this is a sample
+ kargs = urlparse.parse_qs(urlparse.urlparse(url).query)
+
+ if "cdn" in kargs:
+ return kargs["cdn"][0]
+ else:
+ return "_d" #default
+
+ def normalize_segment_url(self, url):
+ """Returns a hash that can uniq a url"""
+
+ return re.sub("authToken=(.*?)(?:&|$)", "-", url)
+
+ def get_segment_type(self, url):
+ """Returns the type of the segment (ad, content, etc)"""
+
+ # Note : This is just a sample of how you can extend
+ # the provider so that rule matching can be done
+ # on different segment types
+ #
+ # if url.startswith("http://asset"):
+ # return "asset"
+ #
+ # if url.startswith("http://ad"):
+ # return "ad"
+
+ return "content"
+
+ def get_tag_kwargs(self, kwargs):
+ """Strip arguments that are not tag relevant and may contain sensitive information"""
+
+ # by default nothing sensative, so return everything
+ return kwargs
+
+ def pull_master_m3u8(self, cid, kwargs):
+ """
+
+ Return the master m3u8 playlist contents. This function is generally outside of httpls_client
+ because to get to the master playlist from an arbitrary cid could require a number of custom steps or custom auth.
+ Additionally all params to the call to dripls are passed, which allows extra parameters to be processed and used
+ for the pulling of the master m3u8 playlist.
+
+ """
+
+ if not cid:
+ raise ValueError("Invalid content token specified")
+
+ # if wt is requested , shortcut and use the local playlist
+ if cid == "wt":
+ return urllib2.urlopen("file://" + self.base_path + "/playlists/wt.m3u8").read()
+
+ raise ValueError("Unable to find provider that can fetch cid = {0} ".format(cid))
+
+
+"""Overwrite the provider with a derived class from HttlsProvider"""
+provider = HttplsProvider()
+
+
1  dripls/conf/dev.py
@@ -0,0 +1 @@
+# Per environment data
1  dripls/conf/local.py
@@ -0,0 +1 @@
+# Per environment data
0  dripls/deploy/__init__.py
No changes.
111 dripls/deploy/fabfile.py
@@ -0,0 +1,111 @@
+"""
+Fabric deploy script
+
+Usage: fab <env> <cmd>
+
+env = dev, stage, prod
+cmd = deploy
+
+Example: fab stage deploy
+
+"""
+import os
+import sys
+import fabric
+from fabric.api import *
+
+env.project = 'dripls'
+env.keys_folder = os.path.join(os.path.dirname(__file__), "keys")
+env.pidfile = 'dripls.pid'
+env.python = 'python'
+env.project_name = 'dripls'
+env.include_wt = False
+
+def sedi(path, before, after):
+ platform = run("uname -s")
+
+ i = "-i " if (platform == "Linux") else "-i ''"
+ run(" sed {0} 's/{1}/{2}/g' {3}".format(i, before.replace("/","\\/"), after.replace("/","\\/"), path ))
+
+def pack():
+
+ ts_include = "--exclude='*.ts'" if not env.include_wt else ""
+ top_path = os.path.dirname(os.path.dirname(__file__))
+
+ with cd(top_path):
+ local("tar czfv /tmp/dripls.tgz --exclude='.git' {0} .".format(ts_include), capture=False)
+
+def deploy_clean():
+ run('mkdir -p {0}'.format(env.path))
+ with cd(env.path):
+ run("find . -type f -not \( -iname '*tag_*' -o -iname '*.ts' \) -exec rm -v '{}' \;")
+
+def deploy():
+
+ pack()
+
+ put('/tmp/dripls.tgz', '/tmp/dripls_remote.tgz')
+
+ with settings(warn_only=True):
+ stop()
+
+ deploy_clean()
+ run('mkdir -p {0}'.format(env.path))
+
+ with cd(env.path):
+ run('tar xzf /tmp/dripls_remote.tgz')
+
+ package_path = os.path.join(env.path,'dripls')
+ with cd(package_path):
+ with settings(warn_only=True):
+ run('rm {0}/conf/env.py'.format(package_path))
+
+ run('ln -s {0}/conf/{1}.py {0}/conf/env.py'.format(package_path, env.env))
+
+ # copy any test segments
+ run('mkdir -p /tmp/dripls_wt_segments/')
+ if env.include_wt:
+ run('mv test/wt_suite/segments/* /tmp/dripls_wt_segments/')
+
+ run('touch /tmp/dripls_wt_segments/touch')
+ run('cp /tmp/dripls_wt_segments/* playlists/')
+
+ sedi("test/wt_suite/local/*", before="{local}", after="file://{0}/test/wt_suite/local".format(package_path))
+ sedi("test/wt_suite/local/*", before="{local_ts}", after="file://{0}/playlists".format(package_path))
+ sedi("test/wt_suite/wt_dripls/*", before="{host}", after="http://{0}".format(env.host))
+ sedi("test/wt_suite/wt_dripls/*", before="{host_ts}", after="http://{0}".format(env.host))
+
+ if env.env == "dev":
+ run('cp test/wt_suite/local/* playlists/')
+ else:
+ run('cp test/wt_suite/wt_dripls/* playlists/')
+
+ start()
+
+def wt():
+ env.include_wt = True
+
+def dev():
+ """
+ Set local environment
+ """
+ env.hosts = ['localhost']
+ env.path = '/tmp/{0}'.format(env.project_name)
+ env.env = 'dev'
+
+ # The proxy behind which dripls is running
+ env.proxy_host = 'http://localhost:8080'
+
+def start():
+ print "Starting"
+ run("{0}/bin/dripls --daemon --app_root_url={1}".format(env.path, env.proxy_host) )
+ print "Started"
+
+def stop():
+ print "Stopping..."
+ run("killall -9 dripls")
+ print "Stopped"
+
+def restart():
+ stop()
+ start()
1  dripls/fabfile.py
@@ -0,0 +1 @@
+from deploy.fabfile import *
95 dripls/httpls_client.py
@@ -0,0 +1,95 @@
+from __future__ import with_statement
+import sys
+import random
+import re
+import shutil
+import urllib
+import urllib2
+import base64
+import subprocess
+import conf.data
+
+def switch_segment(playlist_contents, old_segment, new_segment):
+ """ Replace a segment from the original playlist with a new segment """
+
+ new_contents = playlist_contents.replace(old_segment, new_segment)
+ return new_contents
+
+def store_playlist(playlist_contents, path):
+ """ Store playlist locally """
+ with open(path, "w") as p_file:
+ p_file.writelines(playlist_contents)
+
+def pull_variant_playlist(url):
+ """ Pull a variant playlist's content """
+
+ variant_playlist = {}
+ playlist_response = urllib2.urlopen(url)
+ variant_playlist["url"] = url
+ variant_playlist["content"] = playlist_response.read()
+ variant_playlist["segments"] = {}
+
+ try:
+ segment_counts = {}
+
+ ext = ""
+ for line in variant_playlist["content"].splitlines():
+ if line.startswith("#EXT"):
+ if line.startswith("#EXT-X-KEY:METHOD=AES-"):
+ variant_playlist["key_ext"] = line
+ else:
+ ext = line
+ else:
+ type = conf.data.provider.get_segment_type(line)
+
+ if not "segment" in segment_counts:
+ segment_counts["segment"] = 0
+ else:
+ segment_counts["segment"] += 1
+
+ if not type in segment_counts:
+ segment_counts[type] = 0
+ else:
+ segment_counts[type] += 1
+
+ variant_playlist["segments"][segment_counts["segment"]] = {
+ "url": line,
+ "segment": segment_counts["segment"],
+ "{0}_segment".format(type): segment_counts[type],
+ "type":type,
+ "ext":ext
+ }
+ except:
+ raise RuntimeError("Unable to parse playlist content at url: {0}".format(url) )
+
+ return variant_playlist
+
+def get_variant_playlist_urls(main_playlist):
+ """ Extract variant playlists from the main playlist """
+
+ variant_uris = {}
+ bandwidth = None
+ ext = ""
+
+ try:
+ for line in main_playlist.splitlines():
+ if len(line) > 0:
+ if line.startswith("#EXT"):
+ ext = line
+ for arg in line.split(","):
+ if arg.startswith("BANDWIDTH"):
+ bandwidth = arg.split("=")[1]
+ else:
+ if not bandwidth:
+ continue
+
+ if not bandwidth in variant_uris:
+ variant_uris[str(bandwidth)] = {}
+
+ variant_uris[str(bandwidth)][conf.data.provider.get_cdn_from_playlist_url(line)] = {"url":line, "type":"vplaylist", "bandwidth":bandwidth, "cdn" : conf.data.provider.get_cdn_from_playlist_url(line), "ext":ext}
+ except:
+ raise ValueError("Unable to parse master playlist's content")
+
+ return variant_uris
+
+
146 dripls/main.py
@@ -0,0 +1,146 @@
+
+# Import CherryPy global namespace
+import cherrypy
+import copy
+import urllib
+import urllib2
+import urlparse
+import os.path
+import json
+
+import conf.data
+import httpls_client
+import shaper
+
+import conf
+
+class DriplsController(object):
+ '''DripLS Controller'''
+
+ def on_start(self):
+ pass
+
+ def on_stop(self):
+ pass
+
+ def on_server_start(self):
+ pass
+
+ @cherrypy.expose
+ def ostatus(self, s=None):
+ """ Throw with a specified status """
+
+ raise cherrypy.HTTPError(int(s), message="Custom error raised : {0}".format(s))
+
+ @cherrypy.expose
+ def cache_info(self, cid=None, r=None, tag=None, **kwargs):
+ """ Cache and rewrite a m3u8 """
+
+ info = self.cache_stream(cid, r, tag, kwargs)
+
+ return json.dumps(info, sort_keys=True, indent=4)
+
+ @cherrypy.expose
+ def updatesegment(self, url, new_action):
+ """ Update a previously shaped segment on the fly """
+
+ shaper.update_shaped_segment(url, new_action)
+
+ return "OK"
+
+ @cherrypy.expose
+ def master_m3u8(self, cid=None, r=None, tag=None, **kwargs):
+ """ Cache and stream back a shaped playlist """
+
+ cached_cid = self.cache_stream(cid, r, tag, kwargs)["id"]
+
+ with open("{0}/playlists/m_{1}.m3u8".format(shaper.shaper_store_path, cached_cid), "r") as pf:
+ master_content = pf.read()
+
+ # return the rewritten master
+ cherrypy.response.headers['Content-Type'] = "application/vnd.apple.mpegurl"
+ cherrypy.response.headers['Content-Disposition'] = "inline; filename={0}.m3u8".format(cid)
+
+ return master_content
+
+ @cherrypy.expose
+ def playlist_m3u8(self, p=None, **kwargs):
+ """ Stream back a cached playlist """
+
+ with open("{0}/playlists/{1}.m3u8".format(shaper.shaper_store_path,p), "r") as pf:
+ playlist_content = pf.read()
+
+ cherrypy.response.headers['Content-Type'] = "application/vnd.apple.mpegurl"
+ cherrypy.response.headers['Content-Disposition'] = "inline; filename={0}.m3u8".format(p)
+
+ return playlist_content
+
+ @cherrypy.expose
+ def tag_m3u8(self, tag=None, **kwargs):
+ """ Do the shaping off of a pre-made tag """
+
+ with open("{0}/playlists/tag_{1}".format(shaper.shaper_store_path, tag) , "r") as pf:
+ tag_qs = pf.read()
+
+ #add old keys
+ tag_args = urlparse.parse_qs(tag_qs)
+ for key in tag_args:
+ tag_args[key] = tag_args[key][0]
+
+ #add any new kes that we might have gotten(some might override old keys)
+ for key in kwargs:
+ tag_args[key] = kwargs[key]
+
+ #run master with the tag args
+ return self.master_m3u8(**tag_args);
+
+ def cache_stream(self, cid=None, r=None, tag=None, kwargs=None):
+ """ Perform the actual caching and shaping of the stream """
+
+ rules = shaper.parse_rules(r)
+
+ seeded_content_id = conf.common.get_seeded_cid(cid)
+ master_playlist = conf.data.provider.pull_master_m3u8(cid, kwargs)
+
+ info = shaper.cache_and_shape(master_playlist, seeded_content_id, rules)
+ info["url"] = conf.common.get_final_url("playlist.m3u8","p=m_{0}".format(seeded_content_id))
+
+ # if we have a tag, store
+ if tag:
+ self.store_tag(cid, r, tag, kwargs)
+
+ return info
+
+ def store_tag(self, cid, r, tag, kwargs):
+ """ Store all arguments recieved on the url as the associated tag """
+ tag_args = conf.data.provider.get_tag_kwargs(kwargs)
+ tag_args["cid"] = cid
+
+ if r:
+ tag_args["r"] = r
+
+ with open("{0}/playlists/tag_{1}".format(shaper.shaper_store_path, tag), "w") as pf:
+ pf.write("{0}".format(urllib.urlencode(tag_args)))
+
+conf.dripls_main_site_url = conf.app['root_url']
+root = DriplsController()
+
+current_path = os.path.dirname(os.path.abspath(__file__))
+
+app_config = {
+ '/playlists': {
+ 'tools.staticdir.on': True,
+ 'tools.staticdir.dir': os.path.join(shaper.shaper_store_path, 'playlists'),
+ 'tools.staticdir.content_types': {
+ 'ts': 'video/mp2t',
+ 'm3u8': 'application/vnd.apple.mpegurl'
+ }
+ }
+}
+
+cherrypy.config.update({
+ 'log.error_file': conf.error_log,
+ 'log.screen': True
+})
+
+app = cherrypy.tree.mount(root, '/', app_config)
257 dripls/shaper.py
@@ -0,0 +1,257 @@
+import cherrypy
+import copy
+import logging
+import os.path
+import subprocess
+from subprocess import *
+import Queue
+import urlparse
+import urllib2
+import uuid
+import hashlib
+
+import conf.data
+import conf
+import httpls_client
+
+#port shaping queue
+port_queue = Queue.Queue()
+shaper_store_path = "{0}/".format(os.path.dirname(os.path.realpath(__file__)))
+
+def get_next_shape_port():
+ if port_queue.empty():
+ for port in range(conf.common.shape_start_port, conf.common.shape_end_port):
+ port_queue.put(port)
+
+ # rotate the port
+ port = port_queue.get()
+ port_queue.put(port)
+
+ return port
+
+def generate_status(status):
+ return conf.common.get_final_url("ostatus","s={0}".format(status))
+
+def parse_rules(rule_string):
+ rules = {}
+
+ if not rule_string:
+ return rules
+
+ rule_string = str(rule_string)
+
+ try:
+ for rule in rule_string.split(","):
+ rule_parts = rule.strip().split("~")
+
+ action = rule_parts[1].strip()
+ if not (action.startswith("e") or action.startswith("net")):
+ raise ValueError("Unable to parse rule action {0}".format(action))
+
+ rules[rule_parts[0].strip()] = rule_parts[1].strip()
+ except:
+ raise ValueError("Cannot parse rules : {0}".format(rule_string))
+
+ return rules
+
+def parse_net_rule_action(rule_action):
+ """
+
+ Parse the rule action. Net rule consists of two parts: bandwidth, and packet loss.
+ Format is net<speed>.loss<%packetloss>. Speed is assumed to be in kbs and loss is
+ in percentages.
+
+ """
+
+ # Default to traffic limit exceeding any practical bandwith limitations
+ # Useful for scenarios where only packet loss is provided
+ traffic_limit = 100000
+
+ traffic_loss = 0
+ for netrule in rule_action.split("."):
+ if netrule.startswith("net"):
+ traffic_limit = int(netrule[3:])
+ if netrule.startswith("loss"):
+ traffic_loss = int(netrule[4:])
+
+ return (traffic_limit, traffic_loss)
+
+
+def segment_rule_rewrite(rules, playlist, segment, mock_shape_segment=False):
+ """
+
+ Given a set of rules and a segment in a playlist, find out whether the
+ segment is matched in any of the rules and if so perform the rule action
+
+ Possible rule actions are e - raise HTTP error, net - traffic shape
+
+ """
+
+ # perform rule matching
+ rule_action = segment_rule_match(rules,playlist, segment)
+
+ # no rule match
+ if not rule_action:
+ return None
+
+ # generate error pages if a match found
+ if rule_action.startswith("e"):
+ return generate_status(rule_action[1:])
+
+ if rule_action.startswith("net"):
+ return shape_segment(segment, rule_action, mock_shape_segment=mock_shape_segment)
+
+ raise ValueError( "Cannot match action against appropriate set of actions : {0}".format(rule_action))
+
+def segment_rule_match(rules, playlist, segment):
+ """
+
+ Check if any rule matches the current segment. Generate the playlist/segment possible
+ rule permutation and test for rule hit. Work from more specific rules to more generic rules.
+ If a rule is hit, no further checks will be made.
+
+ """
+
+ non_cdn_bandwidth_key = "{0}k".format(int(playlist["bandwidth"]) / 1000)
+ cdn_bandwidth_key = "{0}.{1}".format(playlist["cdn"], non_cdn_bandwidth_key)
+ cdn_wildcard_key = "{0}.*".format(playlist["cdn"])
+ wildcard_key = "*"
+
+ for bandwidth_key in (cdn_bandwidth_key,non_cdn_bandwidth_key, cdn_wildcard_key, wildcard_key ):
+ check_rules = []
+
+ # playlist match
+ if (segment["type"] == "vplaylist"):
+ if bandwidth_key in rules:
+ return rules[bandwidth_key]
+
+ if "*" in rules:
+ return rules["*"]
+
+ continue
+
+ # handle case of specific type rule
+ segment_type = "{0}_segment".format(segment["type"])
+ check_rules.append("{0}.{1}{2}".format(bandwidth_key, segment["type"][0], segment[segment_type]))
+ check_rules.append("{0}.{1}*".format(bandwidth_key, segment["type"][0]))
+
+ # handle case of general segment rule
+ check_rules.append("{0}.s{1}".format(bandwidth_key, segment["segment"]))
+ check_rules.append("{0}.s*".format(bandwidth_key))
+ check_rules.append("{0}.*".format(bandwidth_key))
+
+ for rule in check_rules:
+ if rule in rules:
+ rule_action = rules[rule].lower()
+
+ logging.debug("matched rule : {0} in segment: {1} ".format(rule, segment["url"]))
+
+ return rule_action
+
+def call_ext_shape_port(port, traffic_limit, traffic_loss, mock_shape_segment):
+ """
+
+ Call the external port shaper script to make sure that the desired rules are set for the port
+
+ Warning: If executing user is not in sudoers, the operation will fail
+
+ """
+
+ shape_cmd = "sudo {0} {1} {2} {3}".format(conf.shaper_path, port, traffic_limit, traffic_loss)
+ logging.info("External shape call : {0} {1}".format(port, shape_cmd))
+
+ if not mock_shape_segment:
+ # execute non-interactive
+ p = subprocess.Popen(["/usr/bin/sudo", "-n", conf.shaper_path, str(port), str(traffic_limit), str(traffic_loss)], stdin=subprocess.PIPE)
+ p.wait()
+
+ if p.returncode != 0:
+ raise SystemError('Executing {0} failed with {1}'.format(conf.shaper_path, p.returncode))
+
+def shape_segment(segment, rule_action, mock_shape_segment=False):
+ """Cache the segment and call the external shaper script"""
+
+ sid = hashlib.sha224(conf.data.provider.normalize_segment_url(segment["url"])).hexdigest()
+
+ #cache the file if it hasn't been cached already
+ s_filename = "{0}playlists/{1}.ts".format(shaper_store_path, sid)
+ if (not os.path.exists(s_filename)):
+ logging.debug("Fetching segment {0} ".format(segment["url"]))
+ segment_content = urllib2.urlopen(segment["url"]).read()
+ logging.debug("Done")
+
+ with open(s_filename, "wb+") as segment_file:
+ segment_file.write(segment_content)
+ segment_file.close()
+
+ with open("{0}.meta".format(s_filename), "wb+") as metadata_file:
+ metadata_file.write(segment["url"])
+ metadata_file.close()
+ else:
+ logging.debug("Segment cached {0} ".format(segment["url"]))
+
+
+ #shape port
+ port = get_next_shape_port()
+ (traffic_limit, traffic_loss) = parse_net_rule_action(rule_action)
+ call_ext_shape_port(port, traffic_limit, traffic_loss, mock_shape_segment)
+
+ #return the final url
+ return conf.common.get_final_url("s/{0}/playlists/{1}.ts".format(port, sid), "" )
+
+def update_shaped_segment(url, rule_action, mock_shape_segment=False):
+ """A request requested an update of a segment post-playlist generation. Handle this here"""
+
+ port_regex = re.search( "/s/(.*?)/" , url)
+
+ if not port_regex:
+ raise("Invalid url. Url must be shaped in order to be re-shaped")
+
+ port = port_regex.group(0).replace("/s/","").rstrip('/')
+ (traffic_limit, traffic_loss) = parse_net_rule_action(rule_action)
+ log.logging("{0} {1}".format(traffic_limit , traffic_loss))
+ call_ext_shape_port(port, traffic_limit, traffic_loss, mock_shape_segment)
+
+def cache_and_shape(master_playlist, seeded_content_id, rules):
+ """Returns shaped m3u8 playlist
+
+ Process and shape a m3u8 playlist based on a set of rules
+
+ """
+
+ shape_info = {}
+ shape_info["id"] = seeded_content_id
+
+ variant_playlists = httpls_client.get_variant_playlist_urls(master_playlist)
+
+ for bitrate in variant_playlists.iterkeys():
+ for alt in variant_playlists[bitrate].iterkeys():
+ variant_playlist_desc = variant_playlists[bitrate][alt]
+ variant_playlist = httpls_client.pull_variant_playlist( variant_playlist_desc["url"])
+
+ # perform rewrite on the variant playlist url to local url or a rule matched url
+ seg_rewrite_url = segment_rule_rewrite(rules, variant_playlist_desc, variant_playlist_desc)
+ local_rewrite_url = conf.common.get_final_url("playlist.m3u8","p=m_{0}_{1}_{2}".format(seeded_content_id, bitrate, alt))
+ master_playlist = httpls_client.switch_segment( master_playlist, variant_playlist["url"], seg_rewrite_url if seg_rewrite_url else local_rewrite_url )
+
+ # don't process a playlist if it hit a rule (ie has been errored out)
+ if seg_rewrite_url:
+ shape_info["{0}.{1}".format(bitrate, alt)] = seg_rewrite_url
+ continue
+
+ # perform rule rewrite on segments within the variant playlist
+ for s in variant_playlist["segments"].iterkeys():
+ seg_rewrite_url = segment_rule_rewrite(rules, variant_playlist_desc, variant_playlist["segments"][s])
+
+ if seg_rewrite_url:
+ variant_playlist["content"] = httpls_client.switch_segment(variant_playlist["content"], variant_playlist["segments"][s]["url"], seg_rewrite_url)
+ variant_playlist["segments"][s]["url"] = seg_rewrite_url
+ shape_info["{0}.{1}.s{2}".format( bitrate, alt, s)] = seg_rewrite_url
+
+ httpls_client.store_playlist(variant_playlist["content"], shaper_store_path + "playlists/m_{0}_{1}_{2}.m3u8".format(seeded_content_id, bitrate, alt))
+
+
+ httpls_client.store_playlist(master_playlist, shaper_store_path + "playlists/m_{0}.m3u8".format(seeded_content_id))
+ return shape_info
+
+
4 dripls/test/__init__.py
@@ -0,0 +1,4 @@
+import os
+import sys
+
+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
21 dripls/test/common_conf_tests.py
@@ -0,0 +1,21 @@
+import unittest
+import os
+
+from conf import common, data
+
+class TestConfCommonStream(unittest.TestCase):
+
+ def setUp(self):
+ return
+
+ def tearDown(self):
+ return
+
+
+ def test_consistent_hash(self):
+ self.assertTrue( data.provider.normalize_segment_url("http://www.test.com/abc") == data.provider.normalize_segment_url("http://www.test.com/abc"))
+ self.assertTrue( data.provider.normalize_segment_url("http://www.test.com/nbc") != data.provider.normalize_segment_url("http://www.test.com/abc"))
+
+ #test stripping of some attributes to achieve consistent hashing
+ self.assertTrue( data.provider.normalize_segment_url("http://www.test.com/abc?authToken=123") == data.provider.normalize_segment_url("http://www.test.com/abc?authToken=321"))
+ return
75 dripls/test/httpls_tests.py
@@ -0,0 +1,75 @@
+import unittest
+import os
+
+import httpls_client
+
+class TestHttpLiveStreamClient(unittest.TestCase):
+ test_path = ""
+
+ def setUp(self):
+ self.test_path = os.path.dirname(os.path.realpath(__file__))
+ return
+
+ def tearDown(self):
+ return
+
+
+ def test_pull_master_playlist(self):
+ master_playlist = open(self.test_path + "/wt_suite/wt_dripls/wt.m3u8","r").read()
+ v_playlists = httpls_client.get_variant_playlist_urls(master_playlist)
+
+ #test with a master playlist
+ c = 0
+ for bandwidth in v_playlists.iterkeys():
+ for cdn in v_playlists[bandwidth].iterkeys():
+ self.assertTrue(v_playlists[bandwidth][cdn]["type"] == "vplaylist")
+ self.assertTrue(v_playlists[bandwidth][cdn]["url"] != None)
+ c += 1
+ self.assertTrue( c > 0)
+
+ # test with a non-master playlist
+ not_master_playlist = open(self.test_path + "/wt_suite/wt_dripls/wt_1700k.m3u8","r").read()
+ v_playlists = httpls_client.get_variant_playlist_urls(not_master_playlist)
+ for bandwidth in v_playlists.iterkeys():
+ for cdn in v_playlists[bandwidth].iterkeys():
+ self.fail("found a variable playlist inside a vplaylist")
+
+ return
+
+ def test_pull_variant_playlist(self):
+
+ # test a read of a vplaylist and its decomposition
+ local_url = "file://" + os.path.abspath(self.test_path + "/wt_suite/basic/wt_1000k_asset_ad.m3u8")
+ v_playlist = httpls_client.pull_variant_playlist(local_url)
+
+ self.assertTrue( v_playlist["url"] != None)
+ self.assertTrue( v_playlist["content"] != None)
+
+ for segment in v_playlist["segments"].iterkeys():
+ self.assertTrue ( v_playlist["segments"][segment]["url"] != None)
+ self.assertTrue ( v_playlist["segments"][segment]["segment"] != None)
+ self.assertTrue ( v_playlist["segments"][segment]["type"] != None)
+
+ if ( v_playlist["segments"][segment]["type"] == "content"):
+ self.assertTrue ( v_playlist["segments"][segment]["content_segment"] != None)
+
+ return
+
+ def test_switch_segment(self):
+
+ # test switch with master playlist
+ master_playlist = open(self.test_path + "/wt_suite/wt_dripls/wt.m3u8","r").read()
+ v_playlists = httpls_client.get_variant_playlist_urls(master_playlist)
+
+ c = 0
+ for bandwidth in v_playlists.iterkeys():
+ for cdn in v_playlists[bandwidth].iterkeys():
+ c += 1
+ replace_segment = "http://testsegment" + str(c)
+ master_playlist = httpls_client.switch_segment(master_playlist, v_playlists[bandwidth][cdn]["url"], replace_segment)
+ self.assertTrue( master_playlist.find(v_playlists[bandwidth][cdn]["url"]) == -1 )
+ self.assertTrue( master_playlist.find( replace_segment) != -1 )
+
+ variant_playlist = open(self.test_path + "/wt_suite/wt_dripls/wt_1700k.m3u8","r").read()
+
+ return
1  dripls/test/nose_with_coverage.sh
@@ -0,0 +1 @@
+ nosetests -s --with-coverage --cover-erase --cover-package=dripls
131 dripls/test/shape_tests.py
@@ -0,0 +1,131 @@
+import unittest
+from urlparse import urlparse
+import os
+
+import shaper, httpls_client, conf
+
+class TestShaper(unittest.TestCase):
+ test_path = ""
+
+ def setUp(self):
+ self.test_path = os.path.dirname(os.path.realpath(__file__))
+
+ for each in os.listdir( "%s/playlists" % self.test_path ):
+ os.remove("%s/playlists/%s" % (self.test_path,each))
+
+ self.master_playlist = open(self.test_path + "/wt_suite/wt_dripls/wt.m3u8","r").read()
+ self.master_loc_playlist = open(self.test_path + "/wt_suite/local/wt.m3u8","r").read().replace("{local}", "file://" + self.test_path + "/wt_suite/local")
+
+ local_url = "file://" + os.path.abspath(self.test_path + "/wt_suite/basic/wt_1000k_asset_ad.m3u8")
+ self.v_playlist_1000k = httpls_client.pull_variant_playlist(local_url)
+ self.v_playlist_1000k["bandwidth"] = 1000000
+ self.v_playlist_1000k["cdn"] = "a"
+ self.v_playlist_1000k["type"] = "vplaylist"
+
+ self.s0_1000k = self.v_playlist_1000k["segments"][0]
+
+ self.s0_1000k_mock = self.v_playlist_1000k["segments"][0].copy()
+ self.s0_1000k_mock["url"] = "file://" + os.path.abspath(self.test_path + "/wt_suite/basic/mock_segment.ts")
+
+ self.c0_1000k = next( self.v_playlist_1000k["segments"][s] for s in self.v_playlist_1000k["segments"] if self.v_playlist_1000k["segments"][s]["type"] == "content" )
+ self.a0_1000k = next( self.v_playlist_1000k["segments"][s] for s in self.v_playlist_1000k["segments"] if self.v_playlist_1000k["segments"][s]["type"] == "ad" )
+ self.as0_1000k = next( self.v_playlist_1000k["segments"][s] for s in self.v_playlist_1000k["segments"] if self.v_playlist_1000k["segments"][s]["type"] == "asset" )
+
+ shaper.shaper_store_path = "%s/" % self.test_path
+ return
+
+ def tearDown(self):
+ return
+
+ def test_rule_parse(self):
+ rules = shaper.parse_rules("*.*~e404")
+ self.assertTrue(rules["*.*"] == "e404")
+ self.assertTrue(shaper.parse_rules(None) == {})
+
+ self.assertRaises(ValueError, shaper.parse_rules, "*.-404")
+
+ #test out [cdn][bitrate][segment]
+ rules = shaper.parse_rules("v.*.*~e404")
+ rules = shaper.parse_rules("v.*.*~net100.loss10")
+
+ #segment multiple rules
+ rules = shaper.parse_rules("v.650k.*~net100.loss10, *~ e404 ,500k~net100")
+ self.assertTrue(rules["*"] == "e404")
+
+ self.assertRaises(ValueError, shaper.parse_rules, "*.c0~e404,*,650k.c0~e404")
+ self.assertRaises(ValueError, shaper.parse_rules, "*.c0~404,*,650k.c0~e404")
+
+ return
+
+ def test_status_gen(self):
+ status_url = shaper.generate_status(404)
+ s = urlparse(status_url)
+ self.assertTrue(s.query == "s=404")
+
+ def test_port_queue(self):
+ p = shaper.get_next_shape_port()
+ p2 = shaper.get_next_shape_port()
+
+ self.assertTrue ( p != None and p2 != None and p != p2)
+
+ def test_segment_rule_match_vplaylist(self):
+ v_playlists_desc = httpls_client.get_variant_playlist_urls(self.master_playlist)
+
+ p4000k = next( v_playlists_desc["4000000"].itervalues())
+ self.assertTrue( shaper.segment_rule_match(shaper.parse_rules("4000k~e404"), p4000k, p4000k) != None )
+ self.assertTrue( shaper.segment_rule_match(shaper.parse_rules("650k~e404"), p4000k, p4000k) == None )
+
+ master_cdn_playlist = open(self.test_path + "/wt_suite/basic/wt_master_cdn_fallback.m3u8","r").read()
+ v_playlists_cdn_desc = httpls_client.get_variant_playlist_urls(master_cdn_playlist)
+
+ p4000k_a = v_playlists_cdn_desc["4000000"]["a"]
+ self.assertTrue( shaper.segment_rule_match(shaper.parse_rules("b.4000k~e404"), p4000k_a, p4000k_a) == None )
+ self.assertTrue( shaper.segment_rule_match(shaper.parse_rules("a.4000k~e404"), p4000k_a, p4000k_a) != None )
+ self.assertTrue( shaper.segment_rule_match(shaper.parse_rules("4000k~e404"), p4000k_a, p4000k_a) != None )
+ self.assertTrue( shaper.segment_rule_match(shaper.parse_rules("*~e404"), p4000k_a, p4000k_a) != None )
+
+ self.assertTrue( shaper.segment_rule_match(shaper.parse_rules("1000k.s0~e404,1000k~e500"), self.v_playlist_1000k, self.v_playlist_1000k) != None )
+
+ def test_segment_rule_match_segment(self):
+ # test rule matching with regular segments
+ self.assertTrue( shaper.segment_rule_match(shaper.parse_rules("1000k~e404"), self.v_playlist_1000k, self.s0_1000k) == None )
+ self.assertTrue( shaper.segment_rule_match(shaper.parse_rules("1000k.s1~e404"), self.v_playlist_1000k, self.s0_1000k) == None )
+ self.assertTrue( shaper.segment_rule_match(shaper.parse_rules("1000k.s0~e404"), self.v_playlist_1000k, self.s0_1000k) == "e404" )
+
+ # test rule matching with content segments
+ self.assertTrue( shaper.segment_rule_match(shaper.parse_rules("1000k.c0~e404"), self.v_playlist_1000k, self.c0_1000k) == "e404" )
+ self.assertTrue( shaper.segment_rule_match(shaper.parse_rules("1000k.*~e404"), self.v_playlist_1000k, self.c0_1000k) == "e404" )
+ self.assertTrue( shaper.segment_rule_match(shaper.parse_rules("1000k.c0~e404"), self.v_playlist_1000k, self.s0_1000k) == None )
+
+ # test rule matching with cdn and segments
+ self.assertTrue( shaper.segment_rule_match(shaper.parse_rules("a.*.c0~e404"), self.v_playlist_1000k, self.c0_1000k) == "e404" )
+ self.assertTrue( shaper.segment_rule_match(shaper.parse_rules("b.*.c0~e404"), self.v_playlist_1000k, self.c0_1000k) == None )
+ self.assertTrue( shaper.segment_rule_match(shaper.parse_rules("a.*.*~e404"), self.v_playlist_1000k, self.c0_1000k) == "e404" )
+
+ self.assertTrue( shaper.segment_rule_match(shaper.parse_rules("a.*.*~net100.loss10"), self.v_playlist_1000k, self.c0_1000k) == "net100.loss10")
+
+ # specific rule takes precedencee
+ self.assertTrue( shaper.segment_rule_match(shaper.parse_rules("a.*.*~net100.loss10,1000k.c0~e404"), self.v_playlist_1000k, self.c0_1000k) == "e404")
+ self.assertTrue( shaper.segment_rule_match(shaper.parse_rules("a.*.*~net100.loss10,1000k.s0~net100"), self.v_playlist_1000k, self.s0_1000k) == "net100")
+
+ def test_segment_shape(self):
+ self.assertTrue( shaper.segment_rule_rewrite(shaper.parse_rules("1000k~e400"), self.v_playlist_1000k, self.v_playlist_1000k) == shaper.generate_status(400))
+
+ self.assertTrue( shaper.segment_rule_rewrite(shaper.parse_rules("1000k.s0~e404"), self.v_playlist_1000k, self.s0_1000k) == shaper.generate_status(404))
+ self.assertTrue( shaper.segment_rule_rewrite(shaper.parse_rules("1000k.s1~e404"), self.v_playlist_1000k, self.s0_1000k) == None)
+
+ self.assertTrue( shaper.segment_rule_rewrite(shaper.parse_rules("1000k.s0~net100.loss10"), self.v_playlist_1000k, self.s0_1000k_mock, mock_shape_segment=True) != None)
+ self.assertTrue( shaper.segment_rule_rewrite(shaper.parse_rules("1000k.*~net100.loss10"),self.v_playlist_1000k, self.s0_1000k_mock, mock_shape_segment=True) != None)
+
+ self.assertRaises(ValueError, shaper.segment_rule_rewrite, {"1000k.*":"404"}, self.v_playlist_1000k, self.s0_1000k_mock)
+
+ def test_cache_and_shape(self):
+ seeded_cid = conf.common.get_seeded_cid("wt")
+
+ shaper.cache_and_shape(self.master_loc_playlist, seeded_cid, shaper.parse_rules("1000k.s0~e404,4000k~e500"))
+
+ self.assertTrue( open("%s/playlists/m_%s.m3u8" % (self.test_path, seeded_cid) ).read().find("s=500") != -1)
+ self.assertTrue( open("%s/playlists/m_%s_1000000__d.m3u8" % (self.test_path, seeded_cid) ).read().find("s=404") != -1)
+
+ pass
+
71 dripls/test/web_tests.py
@@ -0,0 +1,71 @@
+import unittest
+import shutil
+import json
+import os
+
+import shaper, main, httpls_client, conf
+
+class MockHTTPLSProvider(conf.data.HttplsProvider):
+ def __init__(self):
+ self.base_path = os.path.dirname(os.path.realpath(__file__))
+
+ def get_segment_type(self, url):
+ if url.startswith("http://asset"):
+ return "asset"
+
+ if url.startswith("http://ad"):
+ return "ad"
+
+ return "content"
+
+
+conf.data.provider = MockHTTPLSProvider()
+
+class TestShaper(unittest.TestCase):
+ test_path = ""
+
+ def setUp(self):
+ self.test_path = os.path.dirname(os.path.realpath(__file__))
+
+ for each in os.listdir("%s/playlists" % self.test_path):
+ os.remove("%s/playlists/%s" % (self.test_path,each))
+
+ for each in os.listdir("%s/wt_suite/local" % self.test_path):
+ p = open(self.test_path + "/wt_suite/local/%s" % each,"r").read()
+ p = p.replace("{local}", "file://" + self.test_path + "/wt_suite/local")
+
+ with open(self.test_path + "/playlists/%s" % each, "wb+") as pf:
+ pf.write(p)
+
+ shaper.shaper_store_path = "%s/" % self.test_path
+ return
+
+ def tearDown(self):
+ return
+
+ def test_tag(self):
+ service = main.DriplsController()
+ service.cache_info("wt","1000k.s0~e404","tag1",authkey="sample")
+
+ self.assertTrue( open("%s/playlists/tag_tag1" % (self.test_path) ).read().find("authkey=sample&r=1000k.s0%7Ee404&cid=wt") != -1)
+
+ playlist_contents = service.tag_m3u8("tag1")
+ self.assertTrue( playlist_contents != None)
+
+ def test_cache_and_shape_info(self):
+ service = main.DriplsController()
+ info = service.cache_info("wt","1000k.s0~e404","tag1",authkey="sample")
+
+ self.assertTrue( "1000000._d.s0" in info)
+ self.assertTrue( "url" in info)
+
+ playlist_contents = service.playlist_m3u8( "m_%s" % json.loads(info)["id"] )
+ self.assertTrue( playlist_contents != None)
+
+ def test_master_m3u8(self):
+ service = main.DriplsController()
+ playlist_contents = service.master_m3u8("wt", None ,"tag1",authkey="sample")
+
+ self.assertTrue( playlist_contents != None)
+
+
0  dripls/test/wt_suite/basic/mock_segment.ts
No changes.
32 dripls/test/wt_suite/basic/wt_1000k.m3u8
@@ -0,0 +1,32 @@
+#EXTM3U
+#EXT-X-VERSION:2
+#EXT-X-TARGETDURATION:8
+#EXTINF:8,
+http://dripls.hulu.com/ts.ts?s=wt_1000k_final_0000
+#EXTINF:8,
+http://dripls.hulu.com/ts.ts?s=wt_1000k_final_0001
+#EXTINF:8,
+http://dripls.hulu.com/ts.ts?s=wt_1000k_final_0002
+#EXTINF:8,
+http://dripls.hulu.com/ts.ts?s=wt_1000k_final_0003
+#EXTINF:8,
+http://dripls.hulu.com/ts.ts?s=wt_1000k_final_0004
+#EXTINF:8,
+http://dripls.hulu.com/ts.ts?s=wt_1000k_final_0005
+#EXTINF:8,
+http://dripls.hulu.com/ts.ts?s=wt_1000k_final_0006
+#EXTINF:8,
+http://dripls.hulu.com/ts.ts?s=wt_1000k_final_0007
+#EXTINF:8,
+http://dripls.hulu.com/ts.ts?s=wt_1000k_final_0008
+#EXTINF:8,
+http://dripls.hulu.com/ts.ts?s=wt_1000k_final_0009
+#EXTINF:8,
+http://dripls.hulu.com/ts.ts?s=wt_1000k_final_0010
+#EXTINF:8,
+http://dripls.hulu.com/ts.ts?s=wt_1000k_final_0011
+#EXTINF:8,
+http://dripls.hulu.com/ts.ts?s=wt_1000k_final_0012
+#EXTINF:7,
+http://dripls.hulu.com/ts.ts?s=wt_1000k_final_0013
+#EXT-X-ENDLIST
36 dripls/test/wt_suite/basic/wt_1000k_asset_ad.m3u8
@@ -0,0 +1,36 @@
+#EXTM3U
+#EXT-X-VERSION:2
+#EXT-X-TARGETDURATION:8
+#EXTINF:4,
+http://asset?fake=1235
+#EXTINF:8,
+http://dripls.hulu.com/ts.ts?s=wt_1000k_final_0000
+#EXTINF:8,
+http://dripls.hulu.com/ts.ts?s=wt_1000k_final_0001
+#EXTINF:8,
+http://dripls.hulu.com/ts.ts?s=wt_1000k_final_0002
+#EXTINF:8,
+http://dripls.hulu.com/ts.ts?s=wt_1000k_final_0003
+#EXTINF:8,
+http://dripls.hulu.com/ts.ts?s=wt_1000k_final_0004
+#EXTINF:8,
+http://dripls.hulu.com/ts.ts?s=wt_1000k_final_0005
+#EXTINF:8,
+http://dripls.hulu.com/ts.ts?s=wt_1000k_final_0006
+#EXTINF:8,
+http://dripls.hulu.com/ts.ts?s=wt_1000k_final_0007
+#EXTINF:8,
+http://dripls.hulu.com/ts.ts?s=wt_1000k_final_0008
+#EXTINF:8,
+http://dripls.hulu.com/ts.ts?s=wt_1000k_final_0009
+#EXTINF:8,
+http://dripls.hulu.com/ts.ts?s=wt_1000k_final_0010
+#EXTINF:8,
+http://dripls.hulu.com/ts.ts?s=wt_1000k_final_0011
+#EXTINF:4,
+http://ad?fake=1213
+#EXTINF:8,
+http://dripls.hulu.com/ts.ts?s=wt_1000k_final_0012
+#EXTINF:7,
+http://dripls.hulu.com/ts.ts?s=wt_1000k_final_0013
+#EXT-X-ENDLIST
32 dripls/test/wt_suite/basic/wt_1000k_with_cdn.m3u8
@@ -0,0 +1,32 @@
+#EXTM3U
+#EXT-X-VERSION:2
+#EXT-X-TARGETDURATION:8
+#EXTINF:8,
+http://dripls.hulu.com/ts.ts?s=wt_1000k_final_0000
+#EXTINF:8,
+http://dripls.hulu.com/ts.ts?s=wt_1000k_final_0001
+#EXTINF:8,
+http://dripls.hulu.com/ts.ts?s=wt_1000k_final_0002
+#EXTINF:8,
+http://dripls.hulu.com/ts.ts?s=wt_1000k_final_0003
+#EXTINF:8,
+http://dripls.hulu.com/ts.ts?s=wt_1000k_final_0004
+#EXTINF:8,
+http://dripls.hulu.com/ts.ts?s=wt_1000k_final_0005
+#EXTINF:8,
+http://dripls.hulu.com/ts.ts?s=wt_1000k_final_0006
+#EXTINF:8,
+http://dripls.hulu.com/ts.ts?s=wt_1000k_final_0007
+#EXTINF:8,
+http://dripls.hulu.com/ts.ts?s=wt_1000k_final_0008
+#EXTINF:8,
+http://dripls.hulu.com/ts.ts?s=wt_1000k_final_0009
+#EXTINF:8,
+http://dripls.hulu.com/ts.ts?s=wt_1000k_final_0010
+#EXTINF:8,
+http://dripls.hulu.com/ts.ts?s=wt_1000k_final_0011
+#EXTINF:8,
+http://dripls.hulu.com/ts.ts?s=wt_1000k_final_0012
+#EXTINF:7,
+http://dripls.hulu.com/ts.ts?s=wt_1000k_final_0013
+#EXT-X-ENDLIST
12 dripls/test/wt_suite/basic/wt_master_cdn_fallback.m3u8
@@ -0,0 +1,12 @@
+#EXTM3U
+#EXT-X-VERSION:2
+#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=4000000,RESOLUTION=1280x720
+http://dripls.hulu.com/playlist.m3u8?p=wt_4000k&cdn=b
+#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=4000000,RESOLUTION=1280x720
+http://dripls.hulu.com/playlist.m3u8?p=wt_4000k&cdn=a
+#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1000000,RESOLUTION=1280x720
+http://dripls.hulu.com/playlist.m3u8?p=wt_1000k&cdn=a
+#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1000000,RESOLUTION=1280x720
+http://dripls.hulu.com/playlist.m3u8?p=wt_1000k&cdn=b
+#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1700000,RESOLUTION=1280x720
+http://dripls.hulu.com/playlist.m3u8?p=wt_1700k&cdn=a
12 dripls/test/wt_suite/local/wt.m3u8
@@ -0,0 +1,12 @@
+#EXTM3U
+#EXT-X-VERSION:2
+#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=4000000,RESOLUTION=960x540
+{local}/wt_4000k.m3u8
+#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1000000,RESOLUTION=960x540
+{local}/wt_1000k.m3u8
+#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1700000,RESOLUTION=960x540
+{local}/wt_1700k.m3u8
+#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=650000,RESOLUTION=960x540
+{local}/wt_650k.m3u8
+#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=250000,RESOLUTION=960x540
+{local}/wt_250k.m3u8
28 dripls/test/wt_suite/local/wt_1000k.m3u8
@@ -0,0 +1,28 @@
+#EXTM3U
+#EXT-X-VERSION:2
+#EXT-X-TARGETDURATION:9
+#EXTINF:9,
+{local_ts}/wt_1000k_final_0000.ts
+#EXTINF:9,
+{local_ts}/wt_1000k_final_0001.ts
+#EXTINF:9,
+{local_ts}/wt_1000k_final_0002.ts
+#EXTINF:9,
+{local_ts}/wt_1000k_final_0003.ts
+#EXTINF:9,
+{local_ts}/wt_1000k_final_0004.ts
+#EXTINF:9,
+{local_ts}/wt_1000k_final_0005.ts
+#EXTINF:9,
+{local_ts}/wt_1000k_final_0006.ts
+#EXTINF:9,
+{local_ts}/wt_1000k_final_0007.ts
+#EXTINF:9,
+{local_ts}/wt_1000k_final_0008.ts
+#EXTINF:9,
+{local_ts}/wt_1000k_final_0009.ts
+#EXTINF:9,
+{local_ts}/wt_1000k_final_0010.ts
+#EXTINF:9,
+{local_ts}/wt_1000k_final_0011.ts
+#EXT-X-ENDLIST
28 dripls/test/wt_suite/local/wt_1700k.m3u8
@@ -0,0 +1,28 @@
+#EXTM3U
+#EXT-X-VERSION:2
+#EXT-X-TARGETDURATION:9
+#EXTINF:9,
+{local_ts}/wt_1700k_final_0000.ts
+#EXTINF:9,
+{local_ts}/wt_1700k_final_0001.ts
+#EXTINF:9,
+{local_ts}/wt_1700k_final_0002.ts
+#EXTINF:9,
+{local_ts}/wt_1700k_final_0003.ts
+#EXTINF:9,
+{local_ts}/wt_1700k_final_0004.ts
+#EXTINF:9,
+{local_ts}/wt_1700k_final_0005.ts
+#EXTINF:9,
+{local_ts}/wt_1700k_final_0006.ts
+#EXTINF:9,
+{local_ts}/wt_1700k_final_0007.ts
+#EXTINF:9,
+{local_ts}/wt_1700k_final_0008.ts
+#EXTINF:9,
+{local_ts}/wt_1700k_final_0009.ts
+#EXTINF:9,
+{local_ts}/wt_1700k_final_0010.ts
+#EXTINF:9,
+{local_ts}/wt_1700k_final_0011.ts
+#EXT-X-ENDLIST
28 dripls/test/wt_suite/local/wt_250k.m3u8
@@ -0,0 +1,28 @@
+#EXTM3U
+#EXT-X-VERSION:2
+#EXT-X-TARGETDURATION:9
+#EXTINF:9,
+{local_ts}/wt_250k_final_0000.ts
+#EXTINF:9,
+{local_ts}/wt_250k_final_0001.ts
+#EXTINF:9,
+{local_ts}/wt_250k_final_0002.ts
+#EXTINF:9,
+{local_ts}/wt_250k_final_0003.ts
+#EXTINF:9,
+{local_ts}/wt_250k_final_0004.ts
+#EXTINF:9,
+{local_ts}/wt_250k_final_0005.ts
+#EXTINF:9,
+{local_ts}/wt_250k_final_0006.ts
+#EXTINF:9,
+{local_ts}/wt_250k_final_0007.ts
+#EXTINF:9,
+{local_ts}/wt_250k_final_0008.ts
+#EXTINF:9,
+{local_ts}/wt_250k_final_0009.ts
+#EXTINF:9,
+{local_ts}/wt_250k_final_0010.ts
+#EXTINF:9,
+{local_ts}/wt_250k_final_0011.ts
+#EXT-X-ENDLIST
28 dripls/test/wt_suite/local/wt_4000k.m3u8
@@ -0,0 +1,28 @@
+#EXTM3U
+#EXT-X-VERSION:2
+#EXT-X-TARGETDURATION:9
+#EXTINF:9,
+{local_ts}/wt_4000k_final_0000.ts
+#EXTINF:9,
+{local_ts}/wt_4000k_final_0001.ts
+#EXTINF:9,
+{local_ts}/wt_4000k_final_0002.ts
+#EXTINF:9,
+{local_ts}/wt_4000k_final_0003.ts
+#EXTINF:9,
+{local_ts}/wt_4000k_final_0004.ts
+#EXTINF:9,
+{local_ts}/wt_4000k_final_0005.ts
+#EXTINF:9,
+{local_ts}/wt_4000k_final_0006.ts
+#EXTINF:9,
+{local_ts}/wt_4000k_final_0007.ts
+#EXTINF:9,
+{local_ts}/wt_4000k_final_0008.ts
+#EXTINF:9,
+{local_ts}/wt_4000k_final_0009.ts
+#EXTINF:9,
+{local_ts}/wt_4000k_final_0010.ts
+#EXTINF:9,
+{local_ts}/wt_4000k_final_0011.ts
+#EXT-X-ENDLIST
28 dripls/test/wt_suite/local/wt_650k.m3u8
@@ -0,0 +1,28 @@
+#EXTM3U
+#EXT-X-VERSION:2
+#EXT-X-TARGETDURATION:9
+#EXTINF:9,
+{local_ts}/wt_650k_final_0000.ts
+#EXTINF:9,
+{local_ts}/wt_650k_final_0001.ts
+#EXTINF:9,
+{local_ts}/wt_650k_final_0002.ts
+#EXTINF:9,
+{local_ts}/wt_650k_final_0003.ts
+#EXTINF:9,
+{local_ts}/wt_650k_final_0004.ts
+#EXTINF:9,
+{local_ts}/wt_650k_final_0005.ts
+#EXTINF:9,
+{local_ts}/wt_650k_final_0006.ts
+#EXTINF:9,
+{local_ts}/wt_650k_final_0007.ts
+#EXTINF:9,
+{local_ts}/wt_650k_final_0008.ts
+#EXTINF:9,
+{local_ts}/wt_650k_final_0009.ts
+#EXTINF:9,
+{local_ts}/wt_650k_final_0010.ts
+#EXTINF:9,
+{local_ts}/wt_650k_final_0011.ts
+#EXT-X-ENDLIST
BIN  dripls/test/wt_suite/segments/wt_1000k_final_0000.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_1000k_final_0001.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_1000k_final_0002.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_1000k_final_0003.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_1000k_final_0004.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_1000k_final_0005.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_1000k_final_0006.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_1000k_final_0007.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_1000k_final_0008.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_1000k_final_0009.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_1000k_final_0010.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_1000k_final_0011.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_1700k_final_0000.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_1700k_final_0001.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_1700k_final_0002.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_1700k_final_0003.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_1700k_final_0004.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_1700k_final_0005.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_1700k_final_0006.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_1700k_final_0007.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_1700k_final_0008.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_1700k_final_0009.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_1700k_final_0010.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_1700k_final_0011.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_250k_final_0000.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_250k_final_0001.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_250k_final_0002.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_250k_final_0003.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_250k_final_0004.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_250k_final_0005.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_250k_final_0006.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_250k_final_0007.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_250k_final_0008.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_250k_final_0009.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_250k_final_0010.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_250k_final_0011.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_4000k_final_0000.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_4000k_final_0001.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_4000k_final_0002.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_4000k_final_0003.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_4000k_final_0004.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_4000k_final_0005.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_4000k_final_0006.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_4000k_final_0007.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_4000k_final_0008.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_4000k_final_0009.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_4000k_final_0010.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_4000k_final_0011.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_650k_final_0000.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_650k_final_0001.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_650k_final_0002.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_650k_final_0003.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_650k_final_0004.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_650k_final_0005.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_650k_final_0006.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_650k_final_0007.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_650k_final_0008.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_650k_final_0009.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_650k_final_0010.ts
Binary file not shown
BIN  dripls/test/wt_suite/segments/wt_650k_final_0011.ts
Binary file not shown
12 dripls/test/wt_suite/wt_dripls/wt.m3u8
@@ -0,0 +1,12 @@
+#EXTM3U
+#EXT-X-VERSION:2
+#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=4000000,RESOLUTION=960x540
+{host}/playlist.m3u8?p=wt_4000k
+#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1000000,RESOLUTION=960x540
+{host}/playlist.m3u8?p=wt_1000k
+#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1700000,RESOLUTION=960x540
+{host}/playlist.m3u8?p=wt_1700k
+#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=650000,RESOLUTION=960x540
+{host}/playlist.m3u8?p=wt_650k
+#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=250000,RESOLUTION=960x540
+{host}/playlist.m3u8?p=wt_250k
28 dripls/test/wt_suite/wt_dripls/wt_1000k.m3u8
@@ -0,0 +1,28 @@
+#EXTM3U
+#EXT-X-VERSION:2
+#EXT-X-TARGETDURATION:9
+#EXTINF:9,
+{host_ts}/playlists/wt_1000k_final_0000.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_1000k_final_0001.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_1000k_final_0002.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_1000k_final_0003.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_1000k_final_0004.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_1000k_final_0005.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_1000k_final_0006.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_1000k_final_0007.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_1000k_final_0008.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_1000k_final_0009.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_1000k_final_0010.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_1000k_final_0011.ts
+#EXT-X-ENDLIST
28 dripls/test/wt_suite/wt_dripls/wt_1700k.m3u8
@@ -0,0 +1,28 @@
+#EXTM3U
+#EXT-X-VERSION:2
+#EXT-X-TARGETDURATION:9
+#EXTINF:9,
+{host_ts}/playlists/wt_1700k_final_0000.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_1700k_final_0001.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_1700k_final_0002.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_1700k_final_0003.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_1700k_final_0004.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_1700k_final_0005.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_1700k_final_0006.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_1700k_final_0007.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_1700k_final_0008.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_1700k_final_0009.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_1700k_final_0010.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_1700k_final_0011.ts
+#EXT-X-ENDLIST
28 dripls/test/wt_suite/wt_dripls/wt_250k.m3u8
@@ -0,0 +1,28 @@
+#EXTM3U
+#EXT-X-VERSION:2
+#EXT-X-TARGETDURATION:9
+#EXTINF:9,
+{host_ts}/playlists/wt_250k_final_0000.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_250k_final_0001.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_250k_final_0002.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_250k_final_0003.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_250k_final_0004.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_250k_final_0005.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_250k_final_0006.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_250k_final_0007.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_250k_final_0008.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_250k_final_0009.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_250k_final_0010.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_250k_final_0011.ts
+#EXT-X-ENDLIST
28 dripls/test/wt_suite/wt_dripls/wt_4000k.m3u8
@@ -0,0 +1,28 @@
+#EXTM3U
+#EXT-X-VERSION:2
+#EXT-X-TARGETDURATION:9
+#EXTINF:9,
+{host_ts}/playlists/wt_4000k_final_0000.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_4000k_final_0001.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_4000k_final_0002.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_4000k_final_0003.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_4000k_final_0004.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_4000k_final_0005.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_4000k_final_0006.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_4000k_final_0007.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_4000k_final_0008.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_4000k_final_0009.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_4000k_final_0010.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_4000k_final_0011.ts
+#EXT-X-ENDLIST
28 dripls/test/wt_suite/wt_dripls/wt_650k.m3u8
@@ -0,0 +1,28 @@
+#EXTM3U
+#EXT-X-VERSION:2
+#EXT-X-TARGETDURATION:9
+#EXTINF:9,
+{host_ts}/playlists/wt_650k_final_0000.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_650k_final_0001.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_650k_final_0002.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_650k_final_0003.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_650k_final_0004.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_650k_final_0005.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_650k_final_0006.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_650k_final_0007.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_650k_final_0008.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_650k_final_0009.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_650k_final_0010.ts
+#EXTINF:9,
+{host_ts}/playlists/wt_650k_final_0011.ts
+#EXT-X-ENDLIST
1  fabfile.py
@@ -0,0 +1 @@
+from dripls.fabfile import *
26 nginx/default
@@ -0,0 +1,26 @@
+# You may add here your
+# server {
+# ...
+# }
+# statements for each of your virtual hosts
+
+server {
+
+ listen 80; ## listen for ipv4
+ listen [::]:80 default ipv6only=on; ## listen for ipv6
+
+ server_name localhost;
+
+ access_log /var/log/nginx/localhost.access.log;
+
+ location / {
+ proxy_pass http://dripls.hulu.com:8080;
+ }
+
+ location /s {
+ proxy_pass http://localhost:8011; # errors out, maybe there is a better way to error
+ }
+
+ include sites-available/default-locations;
+}
+
6,000 nginx/default-locations
6,000 additions, 0 deletions not shown
1,015 nginx/proxy
@@ -0,0 +1,1015 @@
+server {
+ listen 8887; ## listen for ipv4
+ listen 10000;
+ listen 10001;
+ listen 10002;
+ listen 10003;
+ listen 10004;
+ listen 10005;
+ listen 10006;
+ listen 10007;
+ listen 10008;
+ listen 10009;
+ listen 10010;
+ listen 10011;
+ listen 10012;
+ listen 10013;
+ listen 10014;
+ listen 10015;
+ listen 10016;
+ listen 10017;
+ listen 10018;
+ listen 10019;
+ listen 10020;
+ listen 10021;
+ listen 10022;
+ listen 10023;
+ listen 10024;
+ listen 10025;
+ listen 10026;
+ listen 10027;
+ listen 10028;
+ listen 10029;
+ listen 10030;
+ listen 10031;
+ listen 10032;
+ listen 10033;
+ listen 10034;
+ listen 10035;
+ listen 10036;
+ listen 10037;
+ listen 10038;
+ listen 10039;
+ listen 10040;
+ listen 10041;
+ listen 10042;
+ listen 10043;
+ listen 10044;
+ listen 10045;
+ listen 10046;
+ listen 10047;
+ listen 10048;
+ listen 10049;
+ listen 10050;
+ listen 10051;
+ listen 10052;
+ listen 10053;
+ listen 10054;
+ listen 10055;
+ listen 10056;
+ listen 10057;
+ listen 10058;
+ listen 10059;
+ listen 10060;
+ listen 10061;
+ listen 10062;
+ listen 10063;
+ listen 10064;
+ listen 10065;
+ listen 10066;
+ listen 10067;
+ listen 10068;
+ listen 10069;
+ listen 10070;
+ listen 10071;
+ listen 10072;
+ listen 10073;
+ listen 10074;
+ listen 10075;
+ listen 10076;
+ listen 10077;
+ listen 10078;
+ listen 10079;
+ listen 10080;
+ listen 10081;
+ listen 10082;
+ listen 10083;
+ listen 10084;
+ listen 10085;
+ listen 10086;
+ listen 10087;
+ listen 10088;
+ listen 10089;
+ listen 10090;
+ listen 10091;
+ listen 10092;
+ listen 10093;
+ listen 10094;
+ listen 10095;
+ listen 10096;
+ listen 10097;
+ listen 10098;
+ listen 10099;
+ listen 10100;
+ listen 10101;
+ listen 10102;
+ listen 10103;
+ listen 10104;
+ listen 10105;
+ listen 10106;
+ listen 10107;
+ listen 10108;
+ listen 10109;
+ listen 10110;
+ listen 10111;
+ listen 10112;
+ listen 10113;
+ listen 10114;
+ listen 10115;
+ listen 10116;
+ listen 10117;
+ listen 10118;
+ listen 10119;
+ listen 10120;
+ listen 10121;
+ listen 10122;
+ listen 10123;
+ listen 10124;
+ listen 10125;
+ listen 10126;
+ listen 10127;
+ listen 10128;
+ listen 10129;
+ listen 10130;
+ listen 10131;
+ listen 10132;
+ listen 10133;
+ listen 10134;
+ listen 10135;
+ listen 10136;
+ listen 10137;
+ listen 10138;
+ listen 10139;
+ listen 10140;
+ listen 10141;
+ listen 10142;
+ listen 10143;
+ listen 10144;
+ listen 10145;
+ listen 10146;
+ listen 10147;
+ listen 10148;
+ listen 10149;
+ listen 10150;
+ listen 10151;
+ listen 10152;
+ listen 10153;
+ listen 10154;
+ listen 10155;
+ listen 10156;
+ listen 10157;
+ listen 10158;
+ listen 10159;
+ listen 10160;
+ listen 10161;
+ listen 10162;
+ listen 10163;
+ listen 10164;
+ listen 10165;
+ listen 10166;
+ listen 10167;
+ listen 10168;
+ listen 10169;
+ listen 10170;
+ listen 10171;
+ listen 10172;
+ listen 10173;
+ listen 10174;
+ listen 10175;
+ listen 10176;
+ listen 10177;
+ listen 10178;
+ listen 10179;
+ listen 10180;
+ listen 10181;
+ listen 10182;
+ listen 10183;
+ listen 10184;
+ listen 10185;
+ listen 10186;
+ listen 10187;
+ listen 10188;
+ listen 10189;
+ listen 10190;
+ listen 10191;
+ listen 10192;
+ listen 10193;
+ listen 10194;
+ listen 10195;
+ listen 10196;
+ listen 10197;
+ listen 10198;
+ listen 10199;
+ listen 10200;
+ listen 10201;
+ listen 10202;
+ listen 10203;
+ listen 10204;
+ listen 10205;
+ listen 10206;
+ listen 10207;
+ listen 10208;
+ listen 10209;
+ listen 10210;
+ listen 10211;
+ listen 10212;
+ listen 10213;
+ listen 10214;
+ listen 10215;
+ listen 10216;
+ listen 10217;
+ listen 10218;
+ listen 10219;
+ listen 10220;
+ listen 10221;
+ listen 10222;
+ listen 10223;
+ listen 10224;
+ listen 10225;
+ listen 10226;
+ listen 10227;
+ listen 10228;
+ listen 10229;
+ listen 10230;
+ listen 10231;
+ listen 10232;
+ listen 10233;
+ listen 10234;
+ listen 10235;
+ listen 10236;
+ listen 10237;
+ listen 10238;
+ listen 10239;
+ listen 10240;
+ listen 10241;
+ listen 10242;
+ listen 10243;
+ listen 10244;
+ listen 10245;
+ listen 10246;
+ listen 10247;
+ listen 10248;
+ listen 10249;
+ listen 10250;
+ listen 10251;
+ listen 10252;
+ listen 10253;