├── .github └── dependabot.yml ├── .gitignore ├── README.md ├── app.py └── requirements.txt /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | ignore: 9 | - dependency-name: jinja2 10 | versions: 11 | - 2.11.2 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/code,flask,python 3 | # Edit at https://www.gitignore.io/?templates=code,flask,python 4 | 5 | ### Code ### 6 | .vscode 7 | 8 | ### Flask ### 9 | instance/* 10 | !instance/.gitignore 11 | .webassets-cache 12 | 13 | ### Flask.Python Stack ### 14 | # Byte-compiled / optimized / DLL files 15 | __pycache__/ 16 | *.py[cod] 17 | *$py.class 18 | 19 | # C extensions 20 | *.so 21 | 22 | # Distribution / packaging 23 | .Python 24 | build/ 25 | develop-eggs/ 26 | dist/ 27 | downloads/ 28 | eggs/ 29 | .eggs/ 30 | lib/ 31 | lib64/ 32 | parts/ 33 | sdist/ 34 | var/ 35 | wheels/ 36 | pip-wheel-metadata/ 37 | share/python-wheels/ 38 | *.egg-info/ 39 | .installed.cfg 40 | *.egg 41 | MANIFEST 42 | 43 | # PyInstaller 44 | # Usually these files are written by a python script from a template 45 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 46 | *.manifest 47 | *.spec 48 | 49 | # Installer logs 50 | pip-log.txt 51 | pip-delete-this-directory.txt 52 | 53 | # Unit test / coverage reports 54 | htmlcov/ 55 | .tox/ 56 | .nox/ 57 | .coverage 58 | .coverage.* 59 | .cache 60 | nosetests.xml 61 | coverage.xml 62 | *.cover 63 | .hypothesis/ 64 | .pytest_cache/ 65 | 66 | # Translations 67 | *.mo 68 | *.pot 69 | 70 | # Django stuff: 71 | *.log 72 | local_settings.py 73 | db.sqlite3 74 | db.sqlite3-journal 75 | 76 | # Flask stuff: 77 | instance/ 78 | 79 | # Scrapy stuff: 80 | .scrapy 81 | 82 | # Sphinx documentation 83 | docs/_build/ 84 | 85 | # PyBuilder 86 | target/ 87 | 88 | # Jupyter Notebook 89 | .ipynb_checkpoints 90 | 91 | # IPython 92 | profile_default/ 93 | ipython_config.py 94 | 95 | # pyenv 96 | .python-version 97 | 98 | # pipenv 99 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 100 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 101 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 102 | # install all needed dependencies. 103 | #Pipfile.lock 104 | 105 | # celery beat schedule file 106 | celerybeat-schedule 107 | 108 | # SageMath parsed files 109 | *.sage.py 110 | 111 | # Environments 112 | .env 113 | .venv 114 | env/ 115 | venv/ 116 | ENV/ 117 | env.bak/ 118 | venv.bak/ 119 | 120 | # Spyder project settings 121 | .spyderproject 122 | .spyproject 123 | 124 | # Rope project settings 125 | .ropeproject 126 | 127 | # mkdocs documentation 128 | /site 129 | 130 | # mypy 131 | .mypy_cache/ 132 | .dmypy.json 133 | dmypy.json 134 | 135 | # Pyre type checker 136 | .pyre/ 137 | 138 | ### Python ### 139 | # Byte-compiled / optimized / DLL files 140 | 141 | # C extensions 142 | 143 | # Distribution / packaging 144 | 145 | # PyInstaller 146 | # Usually these files are written by a python script from a template 147 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 148 | 149 | # Installer logs 150 | 151 | # Unit test / coverage reports 152 | 153 | # Translations 154 | 155 | # Django stuff: 156 | 157 | # Flask stuff: 158 | 159 | # Scrapy stuff: 160 | 161 | # Sphinx documentation 162 | 163 | # PyBuilder 164 | 165 | # Jupyter Notebook 166 | 167 | # IPython 168 | 169 | # pyenv 170 | 171 | # pipenv 172 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 173 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 174 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 175 | # install all needed dependencies. 176 | 177 | # celery beat schedule file 178 | 179 | # SageMath parsed files 180 | 181 | # Environments 182 | 183 | # Spyder project settings 184 | 185 | # Rope project settings 186 | 187 | # mkdocs documentation 188 | 189 | # mypy 190 | 191 | # Pyre type checker 192 | 193 | # End of https://www.gitignore.io/api/code,flask,python -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Consume a real-time Twilio Media Stream using WebSockets, Python, and Flask 2 | 3 | Real-time audio from a [Twilio call](https://twilio.com/docs/voice) can be streamed directly to a WebSocket. This repository is part of a [tutorial](https://www.twilio.com/docs/voice/tutorials/consume-real-time-media-stream-using-websockets-python-and-flask). 4 | 5 | Also make sure you check out our [examples repository](https://github.com/twilio/media-streams). 6 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import json 3 | import logging 4 | 5 | from flask import Flask 6 | from flask_sockets import Sockets 7 | 8 | app = Flask(__name__) 9 | sockets = Sockets(app) 10 | 11 | HTTP_SERVER_PORT = 5000 12 | 13 | @sockets.route('/media') 14 | def echo(ws): 15 | app.logger.info("Connection accepted") 16 | # A lot of messages will be sent rapidly. We'll stop showing after the first one. 17 | has_seen_media = False 18 | message_count = 0 19 | while not ws.closed: 20 | message = ws.receive() 21 | if message is None: 22 | app.logger.info("No message received...") 23 | continue 24 | 25 | # Messages are a JSON encoded string 26 | data = json.loads(message) 27 | 28 | # Using the event type you can determine what type of message you are receiving 29 | if data['event'] == "connected": 30 | app.logger.info("Connected Message received: {}".format(message)) 31 | if data['event'] == "start": 32 | app.logger.info("Start Message received: {}".format(message)) 33 | if data['event'] == "media": 34 | if not has_seen_media: 35 | app.logger.info("Media message: {}".format(message)) 36 | payload = data['media']['payload'] 37 | app.logger.info("Payload is: {}".format(payload)) 38 | chunk = base64.b64decode(payload) 39 | app.logger.info("That's {} bytes".format(len(chunk))) 40 | app.logger.info("Additional media messages from WebSocket are being suppressed....") 41 | has_seen_media = True 42 | if data['event'] == "stop": 43 | app.logger.info("Stop Message received: {}".format(message)) 44 | break 45 | message_count += 1 46 | 47 | app.logger.info("Connection closed. Received a total of {} messages".format(message_count)) 48 | 49 | 50 | if __name__ == '__main__': 51 | app.logger.setLevel(logging.DEBUG) 52 | from gevent import pywsgi 53 | from geventwebsocket.handler import WebSocketHandler 54 | 55 | server = pywsgi.WSGIServer(('', HTTP_SERVER_PORT), app, handler_class=WebSocketHandler) 56 | print("Server listening on: http://localhost:" + str(HTTP_SERVER_PORT)) 57 | server.serve_forever() 58 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Click==7.0 2 | Flask==1.1.1 3 | Flask-Sockets==0.2.1 4 | gevent==1.4.0 5 | gevent-websocket==0.10.1 6 | greenlet==0.4.15 7 | itsdangerous==1.1.0 8 | Jinja2==2.10.1 9 | MarkupSafe==1.1.1 10 | Werkzeug==0.15.6 11 | --------------------------------------------------------------------------------