├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── example.py ├── interact_micro_example ├── create_room.py ├── delete_room.py ├── kick_user.py ├── query_room.py └── query_user.py ├── live_examples ├── bandwidth_count.py ├── convert.py ├── create_steam.py ├── history_record.py ├── publish_play_url.py └── save_playback.py ├── pili ├── __init__.py ├── api.py ├── auth.py ├── client.py ├── conf.py ├── errors.py ├── hub.py ├── roomClient.py ├── stream.py ├── urls.py └── utils.py ├── setup.py └── tests ├── test_hub.py └── test_stream.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | 56 | .DS_Store 57 | 58 | test.py 59 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - '2.7' 5 | 6 | install: 7 | - pip install flake8 8 | - pip install pytest 9 | - pip install pytest-cov 10 | - pip install requests 11 | 12 | script: 13 | - make checkstyle 14 | - make test 15 | 16 | deploy: 17 | provider: pypi 18 | user: qiniusdk 19 | password: 20 | secure: MLmZbIXL4kFq0mOi9BprrHTaSNd0ATGTZ3Nw/OjoWlhNw0IaCcaVZgGcGUCE2698zME3YqTzKJ28jZOUKDneNyeBNlai9eS89abbU9onZRczb5xyiMdiqsMks1to+549xjmf/r95SaV/grK350Sy9H5RIMof5iNwCDMugyxnEeg= 21 | on: 22 | tags: true 23 | 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Pili Engineering, Qiniu Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: checkstyle test 2 | 3 | test: 4 | python -m unittest discover tests 5 | 6 | checkstyle: 7 | flake8 --show-source --max-line-length=120 . 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pili Streaming Cloud server-side library for Python 2 | 3 | ## Features 4 | 5 | - URL 6 | - [x] RTMP推流地址: rtmp_publish_url(domain, hub, key, mac, expire_after_seconds) 7 | - [x] RTMP直播地址: rtmp_play_url(domain, hub, key) 8 | - [x] HLS直播地址: hls_play_url(domain, hub, key) 9 | - [x] HDL直播地址: hdl_play_url(domain, hub, key) 10 | - [x] 直播封面地址: snapshot_play_url(domain, hub, key) 11 | - Hub 12 | - [x] 创建流: hub.create(key) 13 | - [x] 获得流: hub.get(key) 14 | - [x] 列出流: hub.list(prefix, limit, marker, liveonly) 15 | - [x] 批量查询直播状态: hub.batch_live_status(streams) 16 | 17 | - Stream 18 | - [x] 流信息: stream.refresh() 19 | - [x] 禁用流: stream.disable(till) 20 | - [x] 启用流: stream.enable() 21 | - [x] 查询直播状态: stream.status() 22 | - [x] 保存直播回放: stream.saveas(start, end, options) 23 | - [x] 保存直播截图: stream.snapshot(options) 24 | - [x] 更改流的实时转码规格: stream.update_converts(profiles) 25 | - [x] 查询直播历史: stream.history(start, end) 26 | 27 | 28 | ## Contents 29 | 30 | - [Installation](#installation) 31 | - [Usage](#usage) 32 | - [Configuration](#configuration) 33 | - [URL](#url) 34 | - [Generate RTMP publish URL](#generate-rtmp-publish-url) 35 | - [Generate RTMP play URL](#generate-rtmp-play-url) 36 | - [Generate HLS play URL](#generate-hls-play-url) 37 | - [Generate HDL play URL](#generate-hdl-play-url) 38 | - [Generate Snapshot play URL](#generate-snapshot-play-url) 39 | - [Hub](#hub) 40 | - [Instantiate a Pili Hub object](#instantiate-a-pili-hub-object) 41 | - [Create a new Stream](#create-a-new-stream) 42 | - [Get a Stream](#get-a-stream) 43 | - [List Streams](#list-streams) 44 | - [List live Streams](#list-live-streams) 45 | - [Batch query live status](#batch-query-live-status) 46 | - [Stream](#stream) 47 | - [Get Stream info](#get-stream-info) 48 | - [Disable a Stream](#disable-a-stream) 49 | - [Enable a Stream](#enable-a-stream) 50 | - [Get Stream live status](#get-stream-live-status) 51 | - [Save Stream live playback](#save-stream-live-playback) 52 | - [Save Stream snapshot](#save-stream-snapshot) 53 | - [Update Stream converts](#update-stream-converts) 54 | - [Get Stream history activity](#get-stream-history-activity) 55 | 56 | ## Installation 57 | 58 | before next step, install git. 59 | 60 | ``` 61 | # install latest version 62 | $ pip install pili2 63 | ``` 64 | 65 | ## Usage 66 | 67 | ### Configuration 68 | 69 | ```python 70 | import pili 71 | 72 | 73 | access_key = "" # 替换成自己 Qiniu 账号的 AccessKey 74 | secret_key = "" # 替换成自己 Qiniu 账号的 SecretKey 75 | hub_name = "" # Hub 必须事先存在 76 | 77 | mac = pili.Mac(AccessKey, SecretKey) 78 | client = pili.Client(mac) 79 | # ... 80 | ``` 81 | 82 | ### URL 83 | 84 | #### Generate RTMP publish URL 85 | 86 | ```python 87 | print "RTMP publish URL:" 88 | print pili.rtmp_publish_url("publish-rtmp.test.com", hub_name, "streamtitle", mac, 60) 89 | # rtmp://publish-rtmp.test.com/PiliSDKTest/streamtitle?e=1488366903&token=Ge_kRfuV_4JW0hOCOnRq5_kD1sX53bKVht8FNdd3:TVrLvhQtDRmww5u1FV5AdwUDRD0= 90 | ``` 91 | 92 | #### Generate RTMP play URL 93 | 94 | ```python 95 | print "RTMP play URL:" 96 | print pili.rtmp_play_url("live-rtmp.test.com", hub_name, "streamtitle") 97 | # rtmp://live-rtmp.test.com/PiliSDKTest/streamtitle 98 | ``` 99 | 100 | #### Generate HLS play URL 101 | 102 | ```python 103 | print "HLS live URL:" 104 | print pili.hls_play_url("live-hls.test.com", hub_name, "streamtitle") 105 | # http://live-hls.test.com/PiliSDKTest/streamtitle.m3u8 106 | ``` 107 | 108 | #### Generate HDL play URL 109 | 110 | ```python 111 | print "HDL live URL:" 112 | print pili.hdl_play_url("live-hdl.test.com", hub_name, "streamtitle") 113 | # http://live-hdl.test.com/PiliSDKTest/streamtitle.flv 114 | ``` 115 | 116 | #### Generate Snapshot play URL 117 | 118 | ```python 119 | print "snapshot URL:" 120 | print pili.snapshot_play_url("live-snapshot.test.com", hub_name, "streamtitle") 121 | # http://live-snapshot.test.com/PiliSDKTest/streamtitle.jpg 122 | ``` 123 | 124 | ### Hub 125 | 126 | #### Instantiate a Pili Hub object 127 | 128 | ```python 129 | mac = pili.Mac(AccessKey, SecretKey) 130 | client = pili.Client(mac) 131 | hub = client.hub("PiliSDKTest") 132 | ``` 133 | 134 | #### Create a new Stream 135 | 136 | ```python 137 | stream = hub.create(stream_title1) 138 | print "create stream:" 139 | print stream 140 | # {"disabledTill": 0, "converts": [], "hub": "PiliSDKTest", "key": "stream29479963631_1"} 141 | ``` 142 | 143 | #### Get a Stream 144 | 145 | ```python 146 | print "get stream:" 147 | stream = hub.get(stream_title1) 148 | print stream 149 | # {"disabledTill": 0, "converts": [], "hub": "PiliSDKTest", "key": "stream29479963631_1"} 150 | ``` 151 | 152 | #### List Streams 153 | 154 | ```python 155 | print "list streams:" 156 | print hub.list(prefix=stream_pre, limit=2) 157 | # {u'marker': u'2', u'items': [{u'key': u'stream23057608792_3'}, {u'key': u'stream23057608792_2'}]} 158 | ``` 159 | 160 | #### List live Streams 161 | 162 | ```python 163 | print "list live streams:" 164 | print hub.list(liveonly=True) 165 | # {u'marker': u'', u'items': [{u'key': u'test1'}]} 166 | ``` 167 | 168 | #### Batch query live status 169 | ```python 170 | print "batch query live streams:", 171 | print hub.batch_live_status(["test1", "test2"]) 172 | # [{u'key': u'test1', u'clientIP': u'172.21.2.14:63672', u'startAt': 1488377320, u'bps': 662200, u'fps': {u'data': 0, u'audio': 47, u'video': 25}}] 173 | ``` 174 | 175 | ### Stream 176 | 177 | #### Get Stream info 178 | 179 | ```python 180 | print "get stream info:" 181 | stream = hub.get(stream_title1) 182 | print stream.refresh() 183 | # {"disabledTill": 0, "converts": [], "hub": "PiliSDKTest", "key": "stream23057608792_1"} 184 | ``` 185 | 186 | #### Disable a Stream 187 | 188 | ```python 189 | stream = hub.get(stream_title1) 190 | print "before disable:", stream, stream.disabled() 191 | stream.disable(int(time.time()) + 5) 192 | print "after call disable:", stream.refresh(), stream.disabled() 193 | time.sleep(5) 194 | print "after sleep 5 seconds:", stream.refresh(), stream.disabled() 195 | 196 | # before disable: {"disabledTill": 0, "converts": [], "hub": "PiliSDKTest", "key": "stream23126041129_1"} False 197 | # after call disable: {"disabledTill": 1488378022, "converts": [], "hub": "PiliSDKTest", "key": "stream23126041129_1"} True 198 | # after sleep 5 seconds: {"disabledTill": 1488378022, "converts": [], "hub": "PiliSDKTest", "key": "stream23126041129_1"} False 199 | ``` 200 | 201 | #### Enable a Stream 202 | 203 | ```python 204 | stream.disable() 205 | stream = hub.get(stream_title1) 206 | stream.disable() 207 | print "before enable:", stream.refresh(), stream.disabled() 208 | stream.enable() 209 | print "after enable:", stream.refresh(), stream.disabled() 210 | # before enable: {"disabledTill": -1, "converts": [], "hub": "PiliSDKTest", "key": "stream23126041129_1"} True 211 | # after enable: {"disabledTill": 0, "converts": [], "hub": "PiliSDKTest", "key": "stream23126041129_1"} False 212 | ``` 213 | 214 | #### Get Stream live status 215 | 216 | ```python 217 | stream = hub.get("test1") 218 | print "query stream live status:" 219 | print stream.status() 220 | # {u'clientIP': u'172.21.2.14:60209', u'startAt': 1488359924, u'bps': 672712, u'fps': {u'data': 0, u'audio': 47, u'video': 25}} 221 | ``` 222 | 223 | #### Save Stream live playback 224 | 225 | ```python 226 | now = int(time.time()) 227 | print "save stream playback:" 228 | print stream.saveas(start_second=now-300, fname="test1.m3u8") 229 | # {u'fname': u'test1.m3u8'} 230 | ``` 231 | 232 | #### Save Stream snapshot 233 | ```python 234 | print "save stream snapshot:" 235 | print stream.snapshot(fname="test1.jpg") 236 | # {u'fname': u'test1.jpg'} 237 | ``` 238 | 239 | #### Update Stream Converts 240 | ```python 241 | stream = hub.get(stream_title1) 242 | print "before update converts:", stream.refresh() 243 | stream.update_converts(["480p", "720p"]) 244 | 245 | print "after update converts:", stream.refresh() 246 | # before update converts: {"disabledTill": 0, "converts": [], "hub": "PiliSDKTest", "key": "stream24038673998_1"} 247 | # after update converts: {"disabledTill": 0, "converts": ["480p", "720p"], "hub": "PiliSDKTest", "key": "stream24038673998_1"} 248 | ``` 249 | 250 | 251 | #### Get Stream history activity 252 | 253 | ```python 254 | now = int(time.time()) 255 | print "get publish history:" 256 | print stream.history(start_second=now-86400) 257 | # [{u'start': 1488359927, u'end': 1488367483}, {u'start': 1488348110, u'end': 1488358759}, {u'start': 1488338678, u'end': 1488340383}, {u'start': 1488333270, u'end': 1488337953}, {u'start': 1488282646, u'end': 1488288321}] 258 | ``` 259 | -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import random 5 | import sys 6 | import time 7 | 8 | import pili 9 | 10 | 11 | def env(key): 12 | if key in os.environ: 13 | return os.environ[key] 14 | else: 15 | return "" 16 | 17 | 18 | if __name__ == "__main__": 19 | if env("PILI_API_HOST") != "": 20 | pili.conf.API_HOST = env("PILI_API_HOST") 21 | 22 | access_key = env("QINIU_ACCESS_KEY") 23 | secret_key = env("QINIU_SECRET_KEY") 24 | if access_key == "" or secret_key == "": 25 | print "need set access_key and secret_key" 26 | sys.exit(1) 27 | 28 | mac = pili.Mac(access_key, secret_key) 29 | 30 | hub_name = "PiliSDKTest" 31 | 32 | client = pili.Client(mac) 33 | hub = client.hub(hub_name) 34 | 35 | stream_pre = "stream2" + str(int(random.random()*1e10)) 36 | stream_title1 = stream_pre + "_1" 37 | stream_title2 = stream_pre + "_2" 38 | stream_title3 = stream_pre + "_3" 39 | 40 | stream = hub.create(stream_title1) 41 | print "create stream:" 42 | print stream 43 | 44 | print "get stream:" 45 | stream = hub.get(stream_title1) 46 | print stream 47 | 48 | print "get stream info:" 49 | stream = hub.get(stream_title1) 50 | print stream.refresh() 51 | 52 | stream = hub.create(stream_title2) 53 | print "create another stream:" 54 | print stream 55 | 56 | hub.create(stream_title3) 57 | 58 | print "list streams:" 59 | print hub.list(prefix=stream_pre, limit=2) 60 | 61 | print "list live streams:" 62 | print hub.list(liveonly=True) 63 | 64 | print "batch query live streams:" 65 | print hub.batch_live_status(["test1", "test2"]) 66 | 67 | stream = hub.get(stream_title1) 68 | print "before disable:", stream, stream.disabled() 69 | stream.disable(int(time.time()) + 5) 70 | print "after call disable:", stream.refresh(), stream.disabled() 71 | time.sleep(5) 72 | print "after sleep 5 seconds:", stream.refresh(), stream.disabled() 73 | 74 | stream = hub.get(stream_title1) 75 | stream.disable() 76 | print "before enable:", stream.refresh(), stream.disabled() 77 | stream.enable() 78 | print "after enable:", stream.refresh(), stream.disabled() 79 | 80 | stream = hub.get(stream_title1) 81 | print "before update converts:", stream.refresh() 82 | stream.update_converts(["480p", "720p"]) 83 | print "after update converts:", stream.refresh() 84 | 85 | stream = hub.get("test1") 86 | print "query stream live status:" 87 | print stream.status() 88 | 89 | now = int(time.time()) 90 | print "save stream playback:" 91 | print stream.saveas(start_second=now-300, fname="test1.m3u8") 92 | 93 | print "save stream snapshot:" 94 | print stream.snapshot(fname="test1.jpg") 95 | 96 | now = int(time.time()) 97 | print "get publish history:" 98 | print stream.history(start_second=now-86400) 99 | 100 | print "RTMP publish URL:" 101 | print pili.rtmp_publish_url("publish-rtmp.test.com", hub_name, "streamtitle", mac, 60) 102 | 103 | print "RTMP play URL:" 104 | print pili.rtmp_play_url("live-rtmp.test.com", hub_name, "streamtitle") 105 | 106 | print "HLS live URL:" 107 | print pili.hls_play_url("live-hls.test.com", hub_name, "streamtitle") 108 | 109 | print "HDL live URL:" 110 | print pili.hdl_play_url("live-hdl.test.com", hub_name, "streamtitle") 111 | 112 | print "snapshot URL:" 113 | print pili.snapshot_play_url("live-snapshot.test.com", hub_name, "streamtitle") 114 | -------------------------------------------------------------------------------- /interact_micro_example/create_room.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from pili import RoomClient, Mac 4 | 5 | # 替换成自己 Qiniu 账号的 AccessKey 6 | access_key = "..." 7 | 8 | # 替换成自己 Qiniu 账号的 SecretKey 9 | secret_key = "..." 10 | 11 | 12 | mac = Mac(access_key, secret_key) 13 | 14 | room = RoomClient(mac) 15 | 16 | print room.createRoom('admin_user', 'roomname') 17 | 18 | # print room.roomToken('roomname', 'admin_user', 'admin', 36000) 19 | -------------------------------------------------------------------------------- /interact_micro_example/delete_room.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from pili import RoomClient, Mac 4 | 5 | # 替换成自己 Qiniu 账号的 AccessKey 6 | access_key = "..." 7 | 8 | # 替换成自己 Qiniu 账号的 SecretKey 9 | secret_key = "..." 10 | 11 | mac = Mac(access_key, secret_key) 12 | 13 | room = RoomClient(mac) 14 | 15 | print room.deleteRoom('roomname') 16 | -------------------------------------------------------------------------------- /interact_micro_example/kick_user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from pili import RoomClient, Mac 4 | 5 | # 替换成自己 Qiniu 账号的 AccessKey 6 | access_key = "..." 7 | 8 | # 替换成自己 Qiniu 账号的 SecretKey 9 | secret_key = "..." 10 | 11 | mac = Mac(access_key, secret_key) 12 | 13 | room = RoomClient(mac) 14 | 15 | print room.kickUser('roomname', 'admin_user') 16 | -------------------------------------------------------------------------------- /interact_micro_example/query_room.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from pili import RoomClient, Mac 4 | 5 | # 替换成自己 Qiniu 账号的 AccessKey 6 | access_key = "..." 7 | 8 | # 替换成自己 Qiniu 账号的 SecretKey 9 | secret_key = "..." 10 | 11 | mac = Mac(access_key, secret_key) 12 | 13 | room = RoomClient(mac) 14 | 15 | print room.getRoom('roomname') 16 | -------------------------------------------------------------------------------- /interact_micro_example/query_user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from pili import RoomClient, Mac 4 | 5 | # 替换成自己 Qiniu 账号的 AccessKey 6 | access_key = "..." 7 | 8 | # 替换成自己 Qiniu 账号的 SecretKey 9 | secret_key = "..." 10 | 11 | mac = Mac(access_key, secret_key) 12 | 13 | room = RoomClient(mac) 14 | 15 | print room.getUser('roomname') 16 | -------------------------------------------------------------------------------- /live_examples/bandwidth_count.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import pili 4 | 5 | # 替换成自己 Qiniu 账号的 AccessKey 6 | access_key = "..." 7 | 8 | 9 | # 替换成自己 Qiniu 账号的 SecretKey 10 | secret_key = "..." 11 | 12 | hub_name = '...' 13 | mac = pili.Mac(access_key, secret_key) 14 | client = pili.Client(mac) 15 | 16 | hub = client.hub(hub_name) 17 | 18 | print hub.bandwidth_count_now() 19 | 20 | # print hub.bandwidth_count_detail(1512616339) 21 | 22 | # print hub.bandwidth_count_history(1512616339, 1512616439) 23 | -------------------------------------------------------------------------------- /live_examples/convert.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import pili 4 | 5 | # 替换成自己 Qiniu 账号的 AccessKey 6 | access_key = "..." 7 | 8 | # 替换成自己 Qiniu 账号的 SecretKey 9 | secret_key = "..." 10 | 11 | hub_name = '' 12 | mac = pili.Mac(access_key, secret_key) 13 | client = pili.Client(mac) 14 | 15 | hub = client.hub(hub_name) 16 | 17 | 18 | stream = hub.get("") 19 | print(stream.update_converts(["480p", "720p"])) 20 | -------------------------------------------------------------------------------- /live_examples/create_steam.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import pili 4 | 5 | # 替换成自己 Qiniu 账号的 AccessKey 6 | access_key = "..." 7 | 8 | # 替换成自己 Qiniu 账号的 SecretKey 9 | secret_key = "..." 10 | 11 | 12 | hub_name = '' 13 | stream_name = '' 14 | 15 | mac = pili.Mac(access_key, secret_key) 16 | 17 | client = pili.Client(mac) 18 | 19 | hub = client.hub(hub_name) 20 | 21 | hub.create(stream_name) 22 | -------------------------------------------------------------------------------- /live_examples/history_record.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import pili 4 | 5 | # 替换成自己 Qiniu 账号的 AccessKey 6 | access_key = "..." 7 | 8 | 9 | # 替换成自己 Qiniu 账号的 SecretKey 10 | secret_key = "..." 11 | 12 | 13 | hub_name = '...' 14 | 15 | stream_name = '...' 16 | 17 | mac = pili.Mac(access_key, secret_key) 18 | client = pili.Client(mac) 19 | hub = client.hub(hub_name) 20 | stream = hub.get(stream_name) 21 | 22 | print(stream.history(start_second=0, end_second=0)) 23 | -------------------------------------------------------------------------------- /live_examples/publish_play_url.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import pili 4 | 5 | # 替换成自己 Qiniu 账号的 AccessKey 6 | access_key = "..." 7 | 8 | # 替换成自己 Qiniu 账号的 SecretKey 9 | secret_key = "..." 10 | 11 | domain = '...' 12 | 13 | hub_name = '...' 14 | 15 | stream_title = '...' 16 | 17 | expire = 3600 18 | 19 | mac = pili.Mac(access_key, secret_key) 20 | client = pili.Client(mac) 21 | 22 | hub = client.hub(hub_name) 23 | 24 | 25 | stream = hub.get("...") 26 | 27 | 28 | print pili.rtmp_publish_url(domain, hub_name, stream_title, mac, expire) 29 | publishKey = '' 30 | print pili.rtmp_publish_url_v1(domain, hub_name, stream_title, expire, publishKey) 31 | 32 | print pili.rtmp_play_url(domain, hub_name, stream_title) 33 | 34 | print pili.hls_play_url(domain, hub_name, stream_title) 35 | 36 | print pili.hdl_play_url(domain, hub_name, stream_title) 37 | 38 | print pili.snapshot_play_url(domain, hub_name, stream_title) 39 | -------------------------------------------------------------------------------- /live_examples/save_playback.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import pili 4 | 5 | access_key = "..." 6 | 7 | 8 | # 替换成自己 Qiniu 账号的 SecretKey 9 | secret_key = "..." 10 | 11 | 12 | hub_name = "..." 13 | 14 | stream_name = "..." 15 | 16 | fname = 'example_fname.mp4' 17 | 18 | mac = pili.Mac(access_key, secret_key) 19 | client = pili.Client(mac) 20 | hub = client.hub(hub_name) 21 | stream = hub.get(stream_name) 22 | 23 | print(stream.saveas(start_second=0, end_second=0, format='mp4', fname=fname)) 24 | -------------------------------------------------------------------------------- /pili/__init__.py: -------------------------------------------------------------------------------- 1 | from .errors import APIError # noqa 2 | from .auth import Mac # noqa 3 | from .client import Client # noqa 4 | from .urls import rtmp_publish_url, rtmp_publish_url_v1, rtmp_play_url, hls_play_url, hdl_play_url, snapshot_play_url # noqa 5 | import conf # noqa 6 | from .roomClient import RoomClient # noqa 7 | -------------------------------------------------------------------------------- /pili/api.py: -------------------------------------------------------------------------------- 1 | from .auth import auth_interface 2 | import pili.conf as conf 3 | from urllib2 import Request 4 | import json 5 | import base64 6 | 7 | 8 | def normalize(args, keyword): 9 | if set(args) - set(keyword): 10 | raise ValueError('invalid key') 11 | for k, v in args.items(): 12 | if v is None: 13 | del args[k] 14 | return args 15 | 16 | 17 | @auth_interface 18 | def delete_room(version, roomName): 19 | url = "http://%s/%s/rooms/%s" % (conf.RTC_API_HOST, version, roomName) 20 | req = Request(url=url) 21 | req.get_method = lambda: 'DELETE' 22 | return req 23 | 24 | 25 | @auth_interface 26 | def get_room(version, roomName): 27 | url = "http://%s/%s/rooms/%s" % (conf.RTC_API_HOST, version, roomName) 28 | return Request(url=url) 29 | 30 | 31 | @auth_interface 32 | def get_user(version, roomName): 33 | url = "http://%s/%s/rooms/%s/users" % (conf.RTC_API_HOST, version, roomName) 34 | return Request(url=url) 35 | 36 | 37 | @auth_interface 38 | def kick_user(version, roomName, userId): 39 | url = "http://%s/%s/rooms/%s/users/%s" % (conf.RTC_API_HOST, version, roomName, userId) 40 | req = Request(url=url) 41 | req.get_method = lambda: 'DELETE' 42 | return req 43 | 44 | 45 | @auth_interface 46 | def create_room(ownerId, version, roomName=None): 47 | params = {'owner_id': ownerId} 48 | url = "http://%s/%s/rooms" % (conf.RTC_API_HOST, version) 49 | if bool(roomName): 50 | params['room_name'] = roomName 51 | encoded = json.dumps(params) 52 | req = Request(url=url, data=encoded) 53 | req.get_method = lambda: 'POST' 54 | return req 55 | 56 | 57 | @auth_interface 58 | def create_stream(hub, **kwargs): 59 | keyword = ['key'] 60 | encoded = json.dumps(normalize(kwargs, keyword)) 61 | url = "http://%s/%s/hubs/%s/streams" % (conf.API_HOST, conf.API_VERSION, hub) 62 | return Request(url=url, data=encoded) 63 | 64 | 65 | @auth_interface 66 | def get_stream(hub, key): 67 | key = base64.urlsafe_b64encode(key) 68 | url = "http://%s/%s/hubs/%s/streams/%s" % (conf.API_HOST, conf.API_VERSION, hub, key) 69 | return Request(url=url) 70 | 71 | 72 | @auth_interface 73 | def get_stream_list(hub, **kwargs): 74 | keyword = ['liveonly', 'prefix', 'limit', 'marker'] 75 | args = normalize(kwargs, keyword) 76 | url = "http://%s/%s/hubs/%s/streams?" % (conf.API_HOST, conf.API_VERSION, hub) 77 | for k, v in args.items(): 78 | url += "&%s=%s" % (k, v) 79 | return Request(url=url) 80 | 81 | 82 | @auth_interface 83 | def batch_live_status(hub, streams): 84 | encoded = json.dumps({"items": streams}) 85 | url = "http://%s/%s/hubs/%s/livestreams?" % (conf.API_HOST, conf.API_VERSION, hub) 86 | return Request(url=url, data=encoded) 87 | 88 | 89 | @auth_interface 90 | def disable_stream(hub, key, till): 91 | key = base64.urlsafe_b64encode(key) 92 | url = "http://%s/%s/hubs/%s/streams/%s/disabled" % (conf.API_HOST, conf.API_VERSION, hub, key) 93 | encoded = json.dumps({"disabledTill": till}) 94 | return Request(url=url, data=encoded) 95 | 96 | 97 | @auth_interface 98 | def get_status(hub, key): 99 | key = base64.urlsafe_b64encode(key) 100 | url = "http://%s/%s/hubs/%s/streams/%s/live" % (conf.API_HOST, conf.API_VERSION, hub, key) 101 | return Request(url=url) 102 | 103 | 104 | @auth_interface 105 | def stream_saveas(hub, key, **kwargs): 106 | keyword = ['start', 'end', 'fname', 'format', 'pipeline', 'notify', 'expireDays'] 107 | encoded = json.dumps(normalize(kwargs, keyword)) 108 | key = base64.urlsafe_b64encode(key) 109 | url = "http://%s/%s/hubs/%s/streams/%s/saveas" % (conf.API_HOST, conf.API_VERSION, hub, key) 110 | return Request(url=url, data=encoded) 111 | 112 | 113 | @auth_interface 114 | def stream_snapshot(hub, key, **kwargs): 115 | keyword = ['time', 'fname', 'format'] 116 | encoded = json.dumps(normalize(kwargs, keyword)) 117 | key = base64.urlsafe_b64encode(key) 118 | url = "http://%s/%s/hubs/%s/streams/%s/snapshot" % (conf.API_HOST, conf.API_VERSION, hub, key) 119 | return Request(url=url, data=encoded) 120 | 121 | 122 | @auth_interface 123 | def get_history(hub, key, **kwargs): 124 | keyword = ['start', 'end'] 125 | args = normalize(kwargs, keyword) 126 | key = base64.urlsafe_b64encode(key) 127 | url = "http://%s/%s/hubs/%s/streams/%s/historyactivity?" % (conf.API_HOST, conf.API_VERSION, hub, key) 128 | for k, v in args.items(): 129 | url += "&%s=%s" % (k, v) 130 | return Request(url=url) 131 | 132 | 133 | @auth_interface 134 | def update_stream_converts(hub, key, profiles): 135 | key = base64.urlsafe_b64encode(key) 136 | url = "http://%s/%s/hubs/%s/streams/%s/converts" % (conf.API_HOST, conf.API_VERSION, hub, key) 137 | encoded = json.dumps({"converts": profiles}) 138 | return Request(url=url, data=encoded) 139 | 140 | 141 | @auth_interface 142 | def bandwidth_count_now(hub): 143 | url = "http://%s/%s/hubs/%s/stat/play" % (conf.API_HOST, conf.API_VERSION, hub) 144 | return Request(url=url) 145 | 146 | 147 | @auth_interface 148 | def bandwidth_count_history(hub, **kwargs): 149 | keyword = ['start', 'end', 'limit', 'marker'] 150 | args = normalize(kwargs, keyword) 151 | url = "http://%s/%s/hubs/%s/stat/play/history" % (conf.API_HOST, conf.API_VERSION, hub) 152 | for k, v in args.items(): 153 | url += "&%s=%s" % (k, v) 154 | return Request(url=url) 155 | 156 | 157 | @auth_interface 158 | def bandwidth_count_detail(hub, time): 159 | url = "http://%s/%s/hubs/%s/stat/play/history/detail?time=%s" % (conf.API_HOST, conf.API_VERSION, hub, time) 160 | return Request(url=url) 161 | -------------------------------------------------------------------------------- /pili/auth.py: -------------------------------------------------------------------------------- 1 | """ 2 | Auth provide class Auth for authentication account. You can use decorator 3 | auth_interface to create a function with auto generated authentication. 4 | """ 5 | from urlparse import urlparse 6 | from .utils import send_and_decode, __hmac_sha1__ 7 | 8 | import pili.conf as conf 9 | 10 | 11 | class Mac(object): 12 | def __init__(self, access_key, secret_key): 13 | if not (access_key and secret_key): 14 | raise ValueError('invalid key') 15 | self.__auth__ = Auth(access_key, secret_key) 16 | 17 | 18 | class Auth(object): 19 | """ 20 | class Auth store the access_key and secret_key for authentication. 21 | """ 22 | def __init__(self, access_key, secret_key): 23 | if not (access_key and secret_key): 24 | raise ValueError('invalid key') 25 | self.access_key, self.secret_key = access_key, secret_key 26 | 27 | def auth_interface_str(self, raw_str): 28 | """ 29 | generate sign str. 30 | """ 31 | encoded = __hmac_sha1__(raw_str, self.secret_key) 32 | return 'Qiniu {0}:{1}'.format(self.access_key, encoded) 33 | 34 | 35 | def auth_interface(method): 36 | """ 37 | decorator takes func(**args) return req and change it to 38 | func(auth, **args) return json result. 39 | 40 | Args: 41 | func(**args) -> Request 42 | 43 | Returns: 44 | func(**args) -> dict (decoded json) 45 | """ 46 | def authed(auth, **args): 47 | """ 48 | send request and decode response. Return the result in python format. 49 | """ 50 | req = method(**args) 51 | parsed = urlparse(req.get_full_url()) 52 | raw_str = '%s %s' % (req.get_method(), parsed.path) 53 | if parsed.query: 54 | raw_str += '?%s' % (parsed.query) 55 | raw_str += '\nHost: %s' % (parsed.netloc) 56 | if req.has_data(): 57 | raw_str += '\nContent-Type: application/json' 58 | raw_str += "\n\n" 59 | if req.has_data(): 60 | raw_str += req.get_data() 61 | req.add_header('Content-Type', 'application/json') 62 | req.add_header('Authorization', auth.auth_interface_str(raw_str)) 63 | req.add_header('User-Agent', conf.API_USERAGENT) 64 | return send_and_decode(req) 65 | return authed 66 | -------------------------------------------------------------------------------- /pili/client.py: -------------------------------------------------------------------------------- 1 | from .hub import Hub 2 | 3 | 4 | class Client(object): 5 | def __init__(self, mac): 6 | self.__mac__ = mac 7 | 8 | def hub(self, hub): 9 | return Hub(self.__mac__, hub) 10 | -------------------------------------------------------------------------------- /pili/conf.py: -------------------------------------------------------------------------------- 1 | """ 2 | Settings 3 | """ 4 | import platform 5 | import sys 6 | 7 | 8 | API_VERSION = 'v2' 9 | API_HOST = 'pili.qiniuapi.com' 10 | 11 | API_USERAGENT = "pili-sdk-python/v2 %s %s" % (platform.python_version(), sys.platform) 12 | RTC_API_VERSION = 'v1' 13 | RTC_API_HOST = 'rtc.qiniuapi.com' 14 | -------------------------------------------------------------------------------- /pili/errors.py: -------------------------------------------------------------------------------- 1 | class APIError(RuntimeError): 2 | def __init__(self, message): 3 | self.message = message 4 | 5 | def __str__(self): 6 | return "%s" % (self.message) 7 | 8 | def __repr__(self): 9 | return self.__str__() 10 | -------------------------------------------------------------------------------- /pili/hub.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import pili.api as api 4 | from .stream import Stream 5 | 6 | 7 | class Hub(object): 8 | def __init__(self, mac, hub): 9 | self.__auth__ = mac.__auth__ 10 | self.__hub__ = hub 11 | 12 | # create 创建一路流 13 | def create(self, key): 14 | api.create_stream(self.__auth__, hub=self.__hub__, key=key) 15 | return Stream(self.__auth__, hub=self.__hub__, key=key) 16 | 17 | # 获取一路流 18 | def get(self, key): 19 | return Stream(self.__auth__, hub=self.__hub__, key=key) 20 | 21 | """ 22 | list 遍历hub的流列表 23 | 输入参数: 24 | prefix: 字符串,匹配的流名前缀 25 | liveonly: 布尔值,可选,如果为True则只列出正在直播的流 26 | limit: 正整数,限定了一次最多可以返回的流个数,实际返回的流个数可能会小于这个值 27 | marker: 字符串,上一次遍历得到的游标 28 | 返回值: 29 | items: 字符串数组,查询返回的流名 30 | marker: 这次遍历得到的游标,下次请求应该带上,如果为"",则表示已遍历完所有流 31 | """ 32 | def list(self, **kwargs): 33 | res = api.get_stream_list(self.__auth__, hub=self.__hub__, **kwargs) 34 | return res 35 | 36 | """ 37 | batch_live_status 批量查询流的直播信息 38 | 输入参数: 39 | streams: 要查询的流名数组,长度不能超过100 40 | 返回值: 如下结构体的数组 41 | key: 流名 42 | startAt: 直播开始的Unix时间戳 43 | clientIP: 推流的客户端IP 44 | bps: 正整数 码率 45 | fps: 46 | audio: 正整数,音频帧率 47 | video: 正整数,视频帧率 48 | data: 正整数,数据帧率 49 | """ 50 | def batch_live_status(self, streams): 51 | res = api.batch_live_status(self.__auth__, hub=self.__hub__, streams=streams) 52 | return res["items"] 53 | 54 | def bandwidth_count_now(self): 55 | res = api.bandwidth_count_now(self.__auth__, hub=self.__hub__) 56 | return res 57 | 58 | def bandwidth_count_history(self, start, end, limit=None, marker=None): 59 | res = api.bandwidth_count_history(self.__auth__, hub=self.__hub__, start=start, end=end, limit=limit, 60 | marker=marker) 61 | return res 62 | 63 | def bandwidth_count_detail(self, time): 64 | res = api.bandwidth_count_detail(self.__auth__, hub=self.__hub__, time=time) 65 | return res 66 | -------------------------------------------------------------------------------- /pili/roomClient.py: -------------------------------------------------------------------------------- 1 | import hmac 2 | import hashlib 3 | import json 4 | import time 5 | import pili.api as api 6 | from utils import urlsafe_base64_encode 7 | 8 | 9 | class RoomClient(object): 10 | """docstring for RoomClient""" 11 | def __init__(self, credentials): 12 | self.__credentials__ = credentials 13 | self.__auth__ = credentials.__auth__ 14 | 15 | def createRoom(self, ownerId, roomName=None, version='v2'): 16 | res = api.create_room(self.__auth__, ownerId=ownerId, roomName=roomName, version=version) 17 | return res 18 | 19 | def getRoom(self, roomName, version='v2'): 20 | res = api.get_room(self.__auth__, roomName=roomName, version=version) 21 | return res 22 | 23 | def deleteRoom(self, roomName, version='v2'): 24 | res = api.delete_room(self.__auth__, roomName=roomName, version=version) 25 | return res 26 | 27 | def getUser(self, roomName, version='v2'): 28 | res = api.get_user(self.__auth__, roomName=roomName, version=version) 29 | return res 30 | 31 | def kickUser(self, roomName, userId, version='v2'): 32 | res = api.kick_user(self.__auth__, roomName=roomName, userId=userId, version=version) 33 | return res 34 | 35 | def roomToken(self, roomName, userId, perm, expireAt, version='v2'): 36 | if version == 'v2': 37 | params = {"version": "2.0", "room_name": roomName, 38 | "user_id": userId, "perm": perm, 39 | "expire_at": int(time.time()) + expireAt} 40 | else: 41 | params = {"room_name": roomName, 42 | "user_id": userId, "perm": perm, 43 | "expire_at": int(time.time()) + expireAt} 44 | 45 | roomAccessString = json.dumps(params, separators=(',', ':')) 46 | encodedRoomAccess = urlsafe_base64_encode(roomAccessString) 47 | hashed = hmac.new(self.__auth__.secret_key, encodedRoomAccess, hashlib.sha1) 48 | encodedSign = urlsafe_base64_encode(hashed.digest()) 49 | return self.__auth__.access_key+":"+encodedSign+":"+encodedRoomAccess 50 | -------------------------------------------------------------------------------- /pili/stream.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import json 4 | import time 5 | 6 | import pili.api as api 7 | 8 | 9 | class Stream(object): 10 | """ 11 | Stream属性 12 | hub: 字符串类型,hub名字 13 | key: 字符串类型,流名 14 | disabledTill: 整型,Unix时间戳,在这之前流均不可用,-1表示永久不可用 15 | converts: 字符串数组,流的转码规格 16 | """ 17 | def __init__(self, auth, hub, key): 18 | self.__auth__ = auth 19 | if not (hub and key): 20 | raise ValueError('invalid key') 21 | self.key = key 22 | self.hub = hub 23 | self.__data__ = None 24 | 25 | def __getattr__(self, attr): 26 | if not self.__data__: 27 | self.refresh() 28 | try: 29 | return self.__data__ if attr == "data" else self.__data__[attr] 30 | except KeyError, e: 31 | return e.message 32 | 33 | def __repr__(self): 34 | return self.to_json() 35 | 36 | # refresh 主动更新流信息,会产生一次rpc调用 37 | def refresh(self): 38 | data = api.get_stream(self.__auth__, hub=self.hub, key=self.key) 39 | self.__data__ = {} 40 | for p in ["disabledTill", "converts"]: 41 | self.__data__[p] = data[p] if p in data else None 42 | self.__data__["key"] = self.key 43 | self.__data__["hub"] = self.hub 44 | return self 45 | 46 | # disable 禁用流,till Unix时间戳,在这之前流均不可用 47 | def disable(self, till=None): 48 | if till is None: 49 | till = -1 50 | return api.disable_stream(self.__auth__, hub=self.hub, key=self.key, till=till) 51 | 52 | # disabled 判断流是否被禁用 53 | def disabled(self): 54 | return self.disabledTill == -1 or self.disabledTill > int(time.time()) 55 | 56 | # enable 开启流 57 | def enable(self): 58 | return api.disable_stream(self.__auth__, hub=self.hub, key=self.key, till=0) 59 | 60 | """ 61 | status 查询直播信息 62 | 返回值: 63 | startAt: 直播开始的Unix时间戳 64 | clientIP: 推流的客户端IP 65 | bps: 正整数 码率 66 | fps: 67 | audio: 正整数,音频帧率 68 | video: 正整数,视频帧率 69 | data: 正整数,数据帧率 70 | """ 71 | def status(self): 72 | res = api.get_status(self.__auth__, hub=self.hub, key=self.key) 73 | return res 74 | 75 | """ 76 | history 查询直播历史 77 | 输入参数: 78 | start_second: Unix时间戳,起始时间,可选,默认不限制起始时间 79 | end_second: Unix时间戳,结束时间,可选,默认为当前时间 80 | 返回值: 如下结构的数组 81 | start: Unix时间戳,直播开始时间 82 | end: Unix时间戳,直播结束时间 83 | """ 84 | def history(self, start_second=None, end_second=None): 85 | res = api.get_history(self.__auth__, hub=self.hub, key=self.key, start=start_second, end=end_second) 86 | return res["items"] 87 | 88 | # save_as等同于saveas接口,出于兼容考虑,暂时保留 89 | def save_as(self, start_second=None, end_second=None, **kwargs): 90 | return self.saveas(start_second, end_second, **kwargs) 91 | 92 | """ 93 | saveas 保存直播回放到存储空间 94 | 输入参数: 95 | start_second: Unix时间戳,起始时间,可选,默认不限制起始时间 96 | end_second: Unix时间戳,结束时间,可选,默认为当前时间 97 | fname: 保存的文件名,可选,不指定会随机生产 98 | format: 保存的文件格式,可选,默认为m3u8,如果指定其他格式则保存动作为异步模式 99 | pipeline: dora的私有队列,可选,不指定则使用默认队列 100 | notify: 保存成功后的回调通知地址 101 | expireDays: 对应ts文件的过期时间 102 | -1 表示不修改ts文件的expire属性 103 | 0 表示修改ts文件生命周期为永久保存 104 | >0 表示修改ts文件的的生命周期为expireDay 105 | 返回值: 106 | fname: 保存到存储空间的文件名 107 | persistentID: 异步模式时,持久化异步处理任务ID,通常用不到该字段 108 | """ 109 | def saveas(self, start_second=None, end_second=None, **kwargs): 110 | kwargs["hub"] = self.hub 111 | kwargs["key"] = self.key 112 | if start_second is not None: 113 | kwargs["start"] = start_second 114 | if end_second is not None: 115 | kwargs["end"] = end_second 116 | res = api.stream_saveas(self.__auth__, **kwargs) 117 | return res 118 | 119 | """ 120 | snapshot 保存直播截图到存储空间 121 | 输入参数: 122 | time: Unix时间戳,要保存的时间点,默认为当前时间 123 | fname: 保存的文件名,可选,不指定会随机生产 124 | format: 保存的文件格式,可选,默认为jpg 125 | 返回值: 126 | fname: 保存到存储空间的文件名 127 | """ 128 | def snapshot(self, **kwargs): 129 | kwargs["hub"] = self.hub 130 | kwargs["key"] = self.key 131 | res = api.stream_snapshot(self.__auth__, **kwargs) 132 | return res 133 | 134 | """ 135 | update_converts 更改流的转码规格 136 | 输入参数: 137 | profiles: 字符串数组,实时转码规格 138 | 返回值: 无 139 | """ 140 | def update_converts(self, profiles=[]): 141 | res = api.update_stream_converts(self.__auth__, hub=self.hub, key=self.key, profiles=profiles) 142 | return res 143 | 144 | def to_json(self): 145 | return json.dumps(self.data) 146 | -------------------------------------------------------------------------------- /pili/urls.py: -------------------------------------------------------------------------------- 1 | from .utils import __hmac_sha1__ 2 | import time 3 | 4 | 5 | def rtmp_publish_url(domain, hub, stream_title, mac, expire): 6 | path = "/%s/%s?e=%d" % (hub, stream_title, time.time()+expire) 7 | token = "%s:%s" % (mac.__auth__.access_key, __hmac_sha1__(path, mac.__auth__.secret_key)) 8 | url = "rtmp://%s%s&token=%s" % (domain, path, token) 9 | return url 10 | 11 | 12 | def rtmp_publish_url_v1(domain, hub, stream_title, expire, publishKey): 13 | path = "/%s/%s?expire=%d" % (hub, stream_title, time.time()+expire) 14 | token = __hmac_sha1__(path, publishKey) 15 | url = "rtmp://%s%s&token=%s" % (domain, path, token) 16 | return url 17 | 18 | 19 | def rtmp_play_url(domain, hub, stream_title): 20 | return "rtmp://%s/%s/%s" % (domain, hub, stream_title) 21 | 22 | 23 | def hls_play_url(domain, hub, stream_title): 24 | return "http://%s/%s/%s.m3u8" % (domain, hub, stream_title) 25 | 26 | 27 | def hdl_play_url(domain, hub, stream_title): 28 | return "http://%s/%s/%s.flv" % (domain, hub, stream_title) 29 | 30 | 31 | def snapshot_play_url(domain, hub, stream_title): 32 | return "http://%s/%s/%s.jpg" % (domain, hub, stream_title) 33 | -------------------------------------------------------------------------------- /pili/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utils 3 | """ 4 | from urllib2 import urlopen, HTTPError 5 | import contextlib 6 | import json 7 | from .errors import APIError 8 | import hmac 9 | import hashlib 10 | import base64 11 | 12 | 13 | def send_and_decode(req): 14 | """ 15 | Send the request and return the decoded json of response. 16 | 17 | Args: 18 | req: urllib2.Request 19 | 20 | Returns: 21 | A dict of decoded response 22 | """ 23 | try: 24 | with contextlib.closing(urlopen(req)) as res: 25 | if res.getcode() == 204: 26 | return None 27 | raw = res.read() 28 | return json.loads(raw) 29 | except HTTPError, res: 30 | raw = res.read() 31 | try: 32 | data = json.loads(raw) 33 | except ValueError: 34 | raise APIError(res.reason) 35 | else: 36 | raise APIError(data["error"]) 37 | 38 | 39 | def __hmac_sha1__(data, key): 40 | """ 41 | hmac-sha1 42 | """ 43 | hashed = hmac.new(key, data, hashlib.sha1) 44 | return base64.urlsafe_b64encode(hashed.digest()) 45 | 46 | 47 | def b(data): 48 | return bytes(data) 49 | 50 | 51 | def s(data): 52 | return bytes(data) 53 | 54 | 55 | def urlsafe_base64_encode(data): 56 | ret = base64.urlsafe_b64encode(b(data)) 57 | return s(ret) 58 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name='pili2', 5 | version='2.1.0', 6 | keywords=('pili', 'streaming', 'hls', 'rtmp'), 7 | description='Pili Streaming Cloud Server-Side Library For Python', 8 | license='MIT License', 9 | install_requires=[], 10 | author='Pili Engineering', 11 | author_email='pili@qiniu.com', 12 | packages=find_packages(), 13 | platforms='any', 14 | ) 15 | -------------------------------------------------------------------------------- /tests/test_hub.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import unittest 5 | 6 | import pili 7 | 8 | 9 | def env(key): 10 | if key in os.environ: 11 | return os.environ[key] 12 | else: 13 | return "" 14 | 15 | 16 | class TestHubCases(unittest.TestCase): 17 | 18 | def setUp(self): 19 | hub_name = "PiliSDKTest" 20 | access_key = env("QINIU_ACCESS_KEY") 21 | secret_key = env("QINIU_SECRET_KEY") 22 | if access_key == "" or secret_key == "": 23 | raise unittest.SkipTest("need set access_key or secret_key") 24 | if env("PILI_API_HOST") != "": 25 | pili.conf.API_HOST = env("PILI_API_HOST") 26 | client = pili.Client(pili.Mac(access_key, secret_key)) 27 | self.hub = client.hub(hub_name) 28 | 29 | # 这个测试case需要保持推流test1 30 | def test_batch_live_status(self): 31 | items = self.hub.batch_live_status(["test1", "test2"]) 32 | self.assertEqual(len(items), 1) 33 | self.assertEqual(items[0]["key"], "test1") 34 | self.assertTrue(items[0]["startAt"] > 0) 35 | self.assertTrue(items[0]["bps"] > 0) 36 | -------------------------------------------------------------------------------- /tests/test_stream.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import random 5 | import time 6 | import unittest 7 | 8 | import pili 9 | 10 | 11 | def env(key): 12 | if key in os.environ: 13 | return os.environ[key] 14 | else: 15 | return "" 16 | 17 | 18 | class TestStreamCases(unittest.TestCase): 19 | 20 | def setUp(self): 21 | hub_name = "PiliSDKTest" 22 | access_key = env("QINIU_ACCESS_KEY") 23 | secret_key = env("QINIU_SECRET_KEY") 24 | if access_key == "" or secret_key == "": 25 | raise unittest.SkipTest("need set access_key or secret_key") 26 | if env("PILI_API_HOST") != "": 27 | pili.conf.API_HOST = env("PILI_API_HOST") 28 | client = pili.Client(pili.Mac(access_key, secret_key)) 29 | self.hub = client.hub(hub_name) 30 | self.stream_title = "streamTest" + str(int(random.random()*1e10)) 31 | 32 | def test_stream_create(self): 33 | stream = self.hub.create(self.stream_title) 34 | self.assertEqual(stream.hub, "PiliSDKTest") 35 | self.assertEqual(stream.key, self.stream_title) 36 | 37 | def test_stream_disable(self): 38 | stream = self.hub.create(self.stream_title) 39 | self.assertFalse(stream.disabled()) 40 | stream.disable() 41 | stream = stream.refresh() 42 | self.assertTrue(stream.disabled()) 43 | stream.disable(int(time.time()) + 1) 44 | stream = stream.refresh() 45 | self.assertTrue(stream.disabled()) 46 | time.sleep(2) 47 | stream = stream.refresh() 48 | self.assertFalse(stream.disabled()) 49 | 50 | def test_stream_converts(self): 51 | stream = self.hub.create(self.stream_title) 52 | self.assertEqual(len(stream.converts), 0) 53 | stream.update_converts(["480p", "720p"]) 54 | stream = stream.refresh() 55 | self.assertEqual(stream.converts, ["480p", "720p"]) 56 | stream.update_converts() 57 | stream = stream.refresh() 58 | self.assertEqual(len(stream.converts), 0) 59 | 60 | # 这个测试需要维持推流test1 61 | def test_stream_saveas(self): 62 | stream = self.hub.get("test1") 63 | stream.save_as() 64 | now = int(time.time()) 65 | stream.save_as(now - 20) 66 | stream.save_as(now - 20, now) 67 | ret = stream.save_as(now - 20, now, fname="test1.mp4", format="mp4") 68 | self.assertEqual(ret["fname"], "test1.mp4") 69 | self.assertTrue(ret["persistentID"]) 70 | try: 71 | stream.save_as(now - 20, now, format="mp4", pipeline="notexist") 72 | except Exception, e: 73 | self.assertEqual(str(e), "no such pipeline") 74 | 75 | # 这个测试需要维持推流test1 76 | def test_stream_snashot(self): 77 | stream = self.hub.get("test1") 78 | ret = stream.snapshot() 79 | self.assertTrue(ret["fname"]) 80 | ret = stream.snapshot(fname="test1.jpg") 81 | self.assertEqual(ret["fname"], "test1.jpg") 82 | 83 | # 这个测试需要维持推流test1 84 | def test_stream_history(self): 85 | stream = self.hub.get("test1") 86 | now = int(time.time()) 87 | ret = stream.history(now - 86400, now) 88 | self.assertTrue(len(ret) > 0) 89 | self.assertTrue(ret[0]["start"] > 0) 90 | self.assertTrue(ret[0]["end"] > 0) 91 | --------------------------------------------------------------------------------