streamlit-webrtc 0.49.4__tar.gz → 0.51.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. streamlit_webrtc-0.51.0/.gitignore +148 -0
  2. {streamlit_webrtc-0.49.4 → streamlit_webrtc-0.51.0}/PKG-INFO +9 -17
  3. streamlit_webrtc-0.51.0/pyproject.toml +77 -0
  4. {streamlit_webrtc-0.49.4 → streamlit_webrtc-0.51.0}/streamlit_webrtc/__init__.py +8 -1
  5. {streamlit_webrtc-0.49.4 → streamlit_webrtc-0.51.0}/streamlit_webrtc/component.py +56 -6
  6. streamlit_webrtc-0.51.0/streamlit_webrtc/credentials.py +75 -0
  7. {streamlit_webrtc-0.49.4 → streamlit_webrtc-0.51.0}/streamlit_webrtc/factory.py +2 -3
  8. {streamlit_webrtc-0.49.4 → streamlit_webrtc-0.51.0}/streamlit_webrtc/mix.py +14 -14
  9. {streamlit_webrtc-0.49.4 → streamlit_webrtc-0.51.0}/streamlit_webrtc/process.py +6 -4
  10. {streamlit_webrtc-0.49.4 → streamlit_webrtc-0.51.0}/streamlit_webrtc/webrtc.py +10 -8
  11. streamlit_webrtc-0.49.4/pyproject.toml +0 -45
  12. {streamlit_webrtc-0.49.4 → streamlit_webrtc-0.51.0}/LICENSE +0 -0
  13. {streamlit_webrtc-0.49.4 → streamlit_webrtc-0.51.0}/README.md +0 -0
  14. {streamlit_webrtc-0.49.4 → streamlit_webrtc-0.51.0}/streamlit_webrtc/_compat.py +0 -0
  15. {streamlit_webrtc-0.49.4 → streamlit_webrtc-0.51.0}/streamlit_webrtc/components_callbacks.py +0 -0
  16. {streamlit_webrtc-0.49.4 → streamlit_webrtc-0.51.0}/streamlit_webrtc/config.py +0 -0
  17. {streamlit_webrtc-0.49.4 → streamlit_webrtc-0.51.0}/streamlit_webrtc/eventloop.py +0 -0
  18. {streamlit_webrtc-0.49.4 → streamlit_webrtc-0.51.0}/streamlit_webrtc/frontend/dist/assets/index-1ywg1u80.js +0 -0
  19. {streamlit_webrtc-0.49.4 → streamlit_webrtc-0.51.0}/streamlit_webrtc/frontend/dist/index.html +0 -0
  20. {streamlit_webrtc-0.49.4 → streamlit_webrtc-0.51.0}/streamlit_webrtc/models.py +0 -0
  21. {streamlit_webrtc-0.49.4 → streamlit_webrtc-0.51.0}/streamlit_webrtc/py.typed +0 -0
  22. {streamlit_webrtc-0.49.4 → streamlit_webrtc-0.51.0}/streamlit_webrtc/receive.py +0 -0
  23. {streamlit_webrtc-0.49.4 → streamlit_webrtc-0.51.0}/streamlit_webrtc/relay.py +0 -0
  24. {streamlit_webrtc-0.49.4 → streamlit_webrtc-0.51.0}/streamlit_webrtc/server.py +0 -0
  25. {streamlit_webrtc-0.49.4 → streamlit_webrtc-0.51.0}/streamlit_webrtc/session_info.py +0 -0
  26. {streamlit_webrtc-0.49.4 → streamlit_webrtc-0.51.0}/streamlit_webrtc/shutdown.py +0 -0
  27. {streamlit_webrtc-0.49.4 → streamlit_webrtc-0.51.0}/streamlit_webrtc/source.py +0 -0
@@ -0,0 +1,148 @@
1
+ ### https://raw.github.com/github/gitignore/218a941be92679ce67d0484547e3e142b2f5f6f0/Python.gitignore
2
+
3
+ # Byte-compiled / optimized / DLL files
4
+ __pycache__/
5
+ *.py[cod]
6
+ *$py.class
7
+
8
+ # C extensions
9
+ *.so
10
+
11
+ # Distribution / packaging
12
+ .Python
13
+ build/
14
+ develop-eggs/
15
+ dist/
16
+ downloads/
17
+ eggs/
18
+ .eggs/
19
+ lib/
20
+ lib64/
21
+ parts/
22
+ sdist/
23
+ var/
24
+ wheels/
25
+ share/python-wheels/
26
+ *.egg-info/
27
+ .installed.cfg
28
+ *.egg
29
+ MANIFEST
30
+
31
+ # PyInstaller
32
+ # Usually these files are written by a python script from a template
33
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
34
+ *.manifest
35
+ *.spec
36
+
37
+ # Installer logs
38
+ pip-log.txt
39
+ pip-delete-this-directory.txt
40
+
41
+ # Unit test / coverage reports
42
+ htmlcov/
43
+ .tox/
44
+ .nox/
45
+ .coverage
46
+ .coverage.*
47
+ .cache
48
+ nosetests.xml
49
+ coverage.xml
50
+ *.cover
51
+ *.py,cover
52
+ .hypothesis/
53
+ .pytest_cache/
54
+ cover/
55
+
56
+ # Translations
57
+ *.mo
58
+ *.pot
59
+
60
+ # Django stuff:
61
+ *.log
62
+ local_settings.py
63
+ db.sqlite3
64
+ db.sqlite3-journal
65
+
66
+ # Flask stuff:
67
+ instance/
68
+ .webassets-cache
69
+
70
+ # Scrapy stuff:
71
+ .scrapy
72
+
73
+ # Sphinx documentation
74
+ docs/_build/
75
+
76
+ # PyBuilder
77
+ .pybuilder/
78
+ target/
79
+
80
+ # Jupyter Notebook
81
+ .ipynb_checkpoints
82
+
83
+ # IPython
84
+ profile_default/
85
+ ipython_config.py
86
+
87
+ # pyenv
88
+ # For a library or package, you might want to ignore these files since the code is
89
+ # intended to run in multiple environments; otherwise, check them in:
90
+ # .python-version
91
+
92
+ # pipenv
93
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
94
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
95
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
96
+ # install all needed dependencies.
97
+ #Pipfile.lock
98
+
99
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow
100
+ __pypackages__/
101
+
102
+ # Celery stuff
103
+ celerybeat-schedule
104
+ celerybeat.pid
105
+
106
+ # SageMath parsed files
107
+ *.sage.py
108
+
109
+ # Environments
110
+ #.env # Not ignore this file because it contains Streamlit specific global settings.
111
+ .venv
112
+ env/
113
+ venv/
114
+ ENV/
115
+ env.bak/
116
+ venv.bak/
117
+
118
+ # Spyder project settings
119
+ .spyderproject
120
+ .spyproject
121
+
122
+ # Rope project settings
123
+ .ropeproject
124
+
125
+ # mkdocs documentation
126
+ /site
127
+
128
+ # mypy
129
+ .mypy_cache/
130
+ .dmypy.json
131
+ dmypy.json
132
+
133
+ # Pyre type checker
134
+ .pyre/
135
+
136
+ # pytype static type analyzer
137
+ .pytype/
138
+
139
+ # Cython debug symbols
140
+ cython_debug/
141
+
142
+ # Streamlit secrets
143
+ .streamlit/secrets.toml
144
+
145
+ # Demo data
146
+ models/
147
+ data/
148
+ *.flv
@@ -1,22 +1,15 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: streamlit-webrtc
3
- Version: 0.49.4
3
+ Version: 0.51.0
4
4
  Summary: Real-time video and audio processing on Streamlit
5
- License: MIT
6
- Author: Yuichiro Tachibana (Tsuchiya)
7
- Author-email: t.yic.yt@gmail.com
8
- Requires-Python: >=3.9, !=2.7.*, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*, !=3.7.*, !=3.8.*
9
- Classifier: License :: OSI Approved :: MIT License
10
- Classifier: Programming Language :: Python :: 3
11
- Classifier: Programming Language :: Python :: 3.9
12
- Classifier: Programming Language :: Python :: 3.10
13
- Classifier: Programming Language :: Python :: 3.11
14
- Classifier: Programming Language :: Python :: 3.12
15
- Classifier: Programming Language :: Python :: 3.13
16
- Requires-Dist: aiortc (>=1.9.0,<2.0.0)
17
- Requires-Dist: packaging (>=20.0)
18
- Requires-Dist: streamlit (>=0.84.1)
19
5
  Project-URL: Repository, https://github.com/whitphx/streamlit-webrtc
6
+ Author-email: "Yuichiro Tachibana (Tsuchiya)" <t.yic.yt@gmail.com>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Requires-Python: !=3.9.7,>=3.9
10
+ Requires-Dist: aiortc>=1.9.0
11
+ Requires-Dist: packaging>=20.0
12
+ Requires-Dist: streamlit>=0.84.1
20
13
  Description-Content-Type: text/markdown
21
14
 
22
15
  # streamlit-webrtc
@@ -353,4 +346,3 @@ See the samples in [app.py](./app.py) for their usage.
353
346
  <a href="https://www.buymeacoffee.com/whitphx" target="_blank" rel="noreferrer"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" width="180" height="50" ></a>
354
347
 
355
348
  [![GitHub Sponsors](https://img.shields.io/github/sponsors/whitphx?label=Sponsor%20me%20on%20GitHub%20Sponsors&style=social)](https://github.com/sponsors/whitphx)
356
-
@@ -0,0 +1,77 @@
1
+ [project]
2
+ name = "streamlit-webrtc"
3
+ version = "0.51.0"
4
+ description = "Real-time video and audio processing on Streamlit"
5
+ authors = [{ name = "Yuichiro Tachibana (Tsuchiya)", email = "t.yic.yt@gmail.com" }]
6
+ requires-python = ">=3.9,!=3.9.7" # 3.9.7 is excluded due to https://github.com/streamlit/streamlit/pull/5168
7
+ readme = "README.md"
8
+ license = "MIT"
9
+ dependencies = [
10
+ # For allow-same-origin, >=0.73.0 is required. See https://blog.streamlit.io/streamlit-components-security-and-a-five-month-quest-to-ship-a-single-line-of-code/
11
+ # 0.84.0 has an error at marshalling component values.
12
+ "streamlit>=0.84.1",
13
+ "aiortc>=1.9.0", # aiortc<1.4.0 causes an error with cryptography>=39.0.0. See https://github.com/whitphx/streamlit-webrtc/issues/1164. The fix was introduced into aiortc in https://github.com/aiortc/aiortc/commit/08b0a7e9f5030a9f7e5617382e92560d4ae763a2 that 1.4.0 included.
14
+ "packaging>=20.0",
15
+ ]
16
+
17
+ [project.urls]
18
+ Repository = "https://github.com/whitphx/streamlit-webrtc"
19
+
20
+ [dependency-groups]
21
+ dev = [
22
+ "click>=7.0",
23
+ "ruff>=0.9.10",
24
+ "mypy[faster-cache]>=1.15.0",
25
+ "pydub>=0.25.1",
26
+ "matplotlib>=3.5.1",
27
+ "streamlit-server-state>=0.17.1",
28
+ "pytest>=7.1.2",
29
+ "opencv-python-headless>=4.5.4.58,<5",
30
+ "numpy>=1.21.0",
31
+ "numpy>=2.1.0 ; python_version >= '3.13'",
32
+ "pandas>=2.0.3",
33
+ "streamlit>=1.13.0",
34
+ "twilio>=8.1",
35
+ "bump-my-version>=1.0.2",
36
+ ]
37
+
38
+ [tool.hatch.build.targets.sdist]
39
+ include = ["/streamlit_webrtc"]
40
+ exclude = ["/streamlit_webrtc/frontend", "!/streamlit_webrtc/frontend/dist"]
41
+
42
+ [tool.hatch.build.targets.wheel]
43
+ include = ["/streamlit_webrtc"]
44
+ exclude = ["/streamlit_webrtc/frontend", "!/streamlit_webrtc/frontend/dist"]
45
+
46
+ [build-system]
47
+ requires = ["hatchling"]
48
+ build-backend = "hatchling.build"
49
+
50
+ [tool.ruff.lint]
51
+ extend-select = ["I"]
52
+
53
+ [tool.bumpversion]
54
+ version = "0.51.0"
55
+ parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
56
+ serialize = ["{major}.{minor}.{patch}"]
57
+ search = "{current_version}"
58
+ replace = "{new_version}"
59
+ regex = false
60
+ ignore_missing_version = false
61
+ ignore_missing_files = false
62
+ tag = true
63
+ sign_tags = false
64
+ tag_name = "v{new_version}"
65
+ tag_message = "Bump version: {current_version} → {new_version}"
66
+ allow_dirty = false
67
+ commit = true
68
+ message = "Bump version: {current_version} → {new_version}"
69
+ commit_args = ""
70
+ setup_hooks = []
71
+ pre_commit_hooks = []
72
+ post_commit_hooks = []
73
+
74
+ [[tool.bumpversion.files]]
75
+ filename = "pyproject.toml"
76
+ search = 'version = "{current_version}"'
77
+ replace = 'version = "{new_version}"'
@@ -22,8 +22,12 @@ from .config import (
22
22
  Translations,
23
23
  VideoHTMLAttributes,
24
24
  )
25
+ from .credentials import (
26
+ get_hf_ice_servers,
27
+ get_twilio_ice_servers,
28
+ )
25
29
  from .factory import create_mix_track, create_process_track, create_video_source_track
26
- from .mix import MixerCallback
30
+ from .mix import MediaStreamMixTrack, MixerCallback
27
31
  from .source import VideoSourceCallback, VideoSourceTrack
28
32
  from .webrtc import (
29
33
  AudioProcessorBase,
@@ -77,9 +81,12 @@ __all__ = [
77
81
  "create_process_track",
78
82
  "create_mix_track",
79
83
  "MixerCallback",
84
+ "MediaStreamMixTrack",
80
85
  "WebRtcStreamerContext",
81
86
  "WebRtcStreamerState",
82
87
  "DEFAULT_AUDIO_HTML_ATTRS",
83
88
  "DEFAULT_MEDIA_STREAM_CONSTRAINTS",
84
89
  "DEFAULT_VIDEO_HTML_ATTRS",
90
+ "get_hf_ice_servers",
91
+ "get_twilio_ice_servers",
85
92
  ]
@@ -1,3 +1,4 @@
1
+ import copy
1
2
  import json
2
3
  import logging
3
4
  import os
@@ -40,6 +41,7 @@ from .config import (
40
41
  Translations,
41
42
  VideoHTMLAttributes,
42
43
  )
44
+ from .credentials import get_hf_ice_servers, get_twilio_ice_servers
43
45
  from .session_info import get_script_run_count, get_this_session_info
44
46
  from .webrtc import (
45
47
  AudioProcessorFactory,
@@ -91,6 +93,7 @@ class WebRtcStreamerContext(Generic[VideoProcessorT, AudioProcessorT]):
91
93
  _worker_ref: "Optional[weakref.ReferenceType[WebRtcWorker[VideoProcessorT, AudioProcessorT]]]" # noqa
92
94
 
93
95
  _component_value_snapshot: Union[ComponentValueSnapshot, None]
96
+ _rtc_configuration: Optional[Union[Dict[str, Any], RTCConfiguration]]
94
97
 
95
98
  def __init__(
96
99
  self,
@@ -100,6 +103,7 @@ class WebRtcStreamerContext(Generic[VideoProcessorT, AudioProcessorT]):
100
103
  self._set_worker(worker)
101
104
  self._set_state(state)
102
105
  self._component_value_snapshot = None
106
+ self._rtc_configuration = None
103
107
 
104
108
  def _set_worker(
105
109
  self, worker: Optional[WebRtcWorker[VideoProcessorT, AudioProcessorT]]
@@ -216,7 +220,7 @@ def compile_state(component_value) -> WebRtcStreamerState:
216
220
  def webrtc_streamer(
217
221
  key: str,
218
222
  mode: WebRtcMode = WebRtcMode.SENDRECV,
219
- rtc_configuration: Optional[Union[Dict, RTCConfiguration]] = None,
223
+ rtc_configuration: Optional[Union[Dict[str, Any], RTCConfiguration]] = None,
220
224
  media_stream_constraints: Optional[Union[Dict, MediaStreamConstraints]] = None,
221
225
  desired_playing_state: Optional[bool] = None,
222
226
  player_factory: Optional[MediaPlayerFactory] = None,
@@ -257,7 +261,7 @@ def webrtc_streamer(
257
261
  def webrtc_streamer(
258
262
  key: str,
259
263
  mode: WebRtcMode = WebRtcMode.SENDRECV,
260
- rtc_configuration: Optional[Union[Dict, RTCConfiguration]] = None,
264
+ rtc_configuration: Optional[Union[Dict[str, Any], RTCConfiguration]] = None,
261
265
  media_stream_constraints: Optional[Union[Dict, MediaStreamConstraints]] = None,
262
266
  desired_playing_state: Optional[bool] = None,
263
267
  player_factory: Optional[MediaPlayerFactory] = None,
@@ -294,7 +298,7 @@ def webrtc_streamer(
294
298
  def webrtc_streamer(
295
299
  key: str,
296
300
  mode: WebRtcMode = WebRtcMode.SENDRECV,
297
- rtc_configuration: Optional[Union[Dict, RTCConfiguration]] = None,
301
+ rtc_configuration: Optional[Union[Dict[str, Any], RTCConfiguration]] = None,
298
302
  media_stream_constraints: Optional[Union[Dict, MediaStreamConstraints]] = None,
299
303
  desired_playing_state: Optional[bool] = None,
300
304
  player_factory: Optional[MediaPlayerFactory] = None,
@@ -331,7 +335,7 @@ def webrtc_streamer(
331
335
  def webrtc_streamer(
332
336
  key: str,
333
337
  mode: WebRtcMode = WebRtcMode.SENDRECV,
334
- rtc_configuration: Optional[Union[Dict, RTCConfiguration]] = None,
338
+ rtc_configuration: Optional[Union[Dict[str, Any], RTCConfiguration]] = None,
335
339
  media_stream_constraints: Optional[Union[Dict, MediaStreamConstraints]] = None,
336
340
  desired_playing_state: Optional[bool] = None,
337
341
  player_factory: Optional[MediaPlayerFactory] = None,
@@ -367,7 +371,7 @@ def webrtc_streamer(
367
371
  def webrtc_streamer(
368
372
  key: str,
369
373
  mode: WebRtcMode = WebRtcMode.SENDRECV,
370
- rtc_configuration: Optional[Union[Dict, RTCConfiguration]] = None,
374
+ rtc_configuration: Optional[Union[Dict[str, Any], RTCConfiguration]] = None,
371
375
  media_stream_constraints: Optional[Union[Dict, MediaStreamConstraints]] = None,
372
376
  desired_playing_state: Optional[bool] = None,
373
377
  player_factory: Optional[MediaPlayerFactory] = None,
@@ -456,6 +460,49 @@ def webrtc_streamer(
456
460
  )
457
461
  st.session_state[key] = context
458
462
 
463
+ if context._rtc_configuration is None:
464
+ context._rtc_configuration = copy.deepcopy(rtc_configuration)
465
+ if context._rtc_configuration is None or (
466
+ isinstance(context._rtc_configuration, dict)
467
+ and context._rtc_configuration.get("iceServers") is None
468
+ ):
469
+ LOGGER.info(
470
+ "rtc_configuration.iceServers is not set. Try to set it automatically."
471
+ )
472
+ if hf_token := os.getenv("HF_TOKEN"):
473
+ LOGGER.info("Try to use TURN server from Hugging Face.")
474
+ try:
475
+ ice_servers = get_hf_ice_servers(hf_token)
476
+ if context._rtc_configuration is None:
477
+ context._rtc_configuration = {}
478
+ LOGGER.info("Successfully got TURN credentials from Hugging Face.")
479
+ context._rtc_configuration["iceServers"] = ice_servers
480
+ except Exception as e:
481
+ LOGGER.error("Failed to get TURN credentials from Hugging Face: %s", e)
482
+ elif os.getenv("TWILIO_ACCOUNT_SID") and os.getenv("TWILIO_AUTH_TOKEN"):
483
+ LOGGER.info("Try to use TURN server from Twilio.")
484
+ twilio_sid = os.getenv("TWILIO_ACCOUNT_SID")
485
+ twilio_token = os.getenv("TWILIO_AUTH_TOKEN")
486
+ try:
487
+ ice_servers = get_twilio_ice_servers(twilio_sid, twilio_token)
488
+ if context._rtc_configuration is None:
489
+ context._rtc_configuration = {}
490
+ LOGGER.info("Successfully got TURN credentials from Twilio.")
491
+ context._rtc_configuration["iceServers"] = ice_servers
492
+ except Exception as e:
493
+ LOGGER.error("Failed to get TURN credentials from Twilio: %s", e)
494
+ else:
495
+ LOGGER.info("Use STUN server from Google.")
496
+ # TODO: Check network reachability and unset ice_servers if failed
497
+ ice_servers = [{"urls": "stun:stun.l.google.com:19302"}]
498
+ if ice_servers:
499
+ if context._rtc_configuration is None:
500
+ context._rtc_configuration = {}
501
+ LOGGER.info("Successfully got STUN server from Google.")
502
+ context._rtc_configuration["iceServers"] = [
503
+ {"urls": "stun:stun.l.google.com:19302"}
504
+ ]
505
+
459
506
  webrtc_worker = context._get_worker()
460
507
 
461
508
  sdp_answer_json = None
@@ -493,7 +540,7 @@ def webrtc_streamer(
493
540
  sdp_answer_json=sdp_answer_json,
494
541
  mode=mode.name,
495
542
  settings=client_settings,
496
- rtc_configuration=rtc_configuration,
543
+ rtc_configuration=context._rtc_configuration,
497
544
  media_stream_constraints=media_stream_constraints,
498
545
  video_html_attrs=video_html_attrs,
499
546
  audio_html_attrs=audio_html_attrs,
@@ -549,6 +596,9 @@ def webrtc_streamer(
549
596
  webrtc_worker.stop()
550
597
  context._set_worker(None)
551
598
  webrtc_worker = None
599
+
600
+ context._rtc_configuration = None
601
+
552
602
  # Rerun to unset the SDP answer from the frontend args
553
603
  rerun()
554
604
 
@@ -0,0 +1,75 @@
1
+ """
2
+ MIT License
3
+
4
+ Copyright (c) 2024 Freddy Boulton
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
23
+ """
24
+ # Original: https://github.com/freddyaboulton/fastrtc/blob/66f0a81b76684c5d58761464fb67642891066f93/LICENSE
25
+
26
+ import json
27
+ import os
28
+ import urllib.error
29
+ import urllib.request
30
+ from typing import Optional
31
+
32
+
33
+ def get_hf_ice_servers(token: Optional[str] = None):
34
+ if token is None:
35
+ token = os.getenv("HF_TOKEN")
36
+
37
+ if token is None:
38
+ raise ValueError("HF_TOKEN is not set")
39
+
40
+ req = urllib.request.Request(
41
+ "https://fastrtc-turn-server-login.hf.space/credentials",
42
+ headers={"X-HF-Access-Token": token},
43
+ )
44
+ try:
45
+ with urllib.request.urlopen(req) as response:
46
+ if response.status != 200:
47
+ raise ValueError("Failed to get credentials from HF turn server")
48
+ credentials = json.loads(response.read())
49
+ [
50
+ {
51
+ "urls": "turn:gradio-turn.com:80",
52
+ **credentials,
53
+ },
54
+ ]
55
+ except urllib.error.URLError:
56
+ raise ValueError("Failed to get credentials from HF turn server")
57
+
58
+
59
+ def get_twilio_ice_servers(
60
+ twilio_sid: Optional[str] = None, twilio_token: Optional[str] = None
61
+ ):
62
+ try:
63
+ from twilio.rest import Client
64
+ except ImportError:
65
+ raise ImportError("Please install twilio with `pip install twilio`")
66
+
67
+ if not twilio_sid and not twilio_token:
68
+ twilio_sid = os.environ.get("TWILIO_ACCOUNT_SID")
69
+ twilio_token = os.environ.get("TWILIO_AUTH_TOKEN")
70
+
71
+ client = Client(twilio_sid, twilio_token)
72
+
73
+ token = client.tokens.create()
74
+
75
+ return token.ice_servers
@@ -1,7 +1,6 @@
1
- from typing import Literal, Optional, Union, overload
1
+ from typing import Literal, Optional, Type, Union, overload
2
2
 
3
3
  import streamlit as st
4
- from aiortc import MediaStreamTrack
5
4
 
6
5
  from .eventloop import get_global_event_loop, loop_context
7
6
  from .mix import MediaStreamMixTrack, MixerCallback
@@ -33,7 +32,7 @@ _PROCESSOR_TRACK_CACHE_KEY_PREFIX = "__PROCESSOR_TRACK_CACHE__"
33
32
 
34
33
  def _get_track_class(
35
34
  kind: Literal["video", "audio"], async_processing: bool
36
- ) -> MediaStreamTrack:
35
+ ) -> Union[Type[MediaProcessTrack], Type[AsyncMediaProcessTrack]]:
37
36
  if kind == "video":
38
37
  if async_processing:
39
38
  return AsyncVideoProcessTrack
@@ -8,12 +8,14 @@ import time
8
8
  import traceback
9
9
  import weakref
10
10
  from collections import OrderedDict
11
- from typing import Callable, Generic, List, NamedTuple, Optional, Union
11
+ from typing import Callable, Generic, List, NamedTuple, Optional, Union, cast
12
12
 
13
13
  import av
14
14
  from aiortc import MediaStreamTrack
15
15
  from aiortc.contrib.media import RelayStreamTrack
16
16
  from aiortc.mediastreams import MediaStreamError
17
+ from av.frame import Frame
18
+ from av.packet import Packet
17
19
 
18
20
  from .eventloop import get_global_event_loop, loop_context
19
21
  from .models import FrameT
@@ -37,12 +39,11 @@ VIDEO_TIME_BASE = fractions.Fraction(1, VIDEO_CLOCK_RATE)
37
39
 
38
40
 
39
41
  MixerCallback = Callable[[List[FrameT]], FrameT]
40
- Frame = Union[av.VideoFrame, av.AudioFrame]
41
42
 
42
43
 
43
44
  class InputQueueItem(NamedTuple):
44
- source_track_id: int
45
- frame: Optional[Frame]
45
+ source_track_id: str
46
+ frame: Optional[Union[Frame, Packet]]
46
47
 
47
48
 
48
49
  async def input_track_coro(
@@ -99,10 +100,10 @@ async def mix_coro(mix_track: "MediaStreamMixTrack"):
99
100
  if output_frame.pts is None and output_frame.time_base is None:
100
101
  timestamp = time.monotonic() - started_at
101
102
  if isinstance(output_frame, av.VideoFrame):
102
- output_frame.pts = timestamp * VIDEO_CLOCK_RATE
103
+ output_frame.pts = int(timestamp * VIDEO_CLOCK_RATE)
103
104
  output_frame.time_base = VIDEO_TIME_BASE
104
105
  elif isinstance(output_frame, av.AudioFrame):
105
- output_frame.pts = timestamp * AUDIO_SAMPLE_RATE
106
+ output_frame.pts = int(timestamp * AUDIO_SAMPLE_RATE)
106
107
  output_frame.time_base = AUDIO_TIME_BASE
107
108
 
108
109
  except Exception:
@@ -128,7 +129,7 @@ class MediaStreamMixTrack(MediaStreamTrack, Generic[FrameT]):
128
129
  _input_queue: asyncio.Queue
129
130
  _queue: "asyncio.Queue[Optional[Frame]]"
130
131
  _latest_frames_map: (
131
- "weakref.WeakKeyDictionary[RelayStreamTrack, Union[Frame, None]]"
132
+ "weakref.WeakKeyDictionary[RelayStreamTrack, Union[Frame, Packet, None]]"
132
133
  )
133
134
  _latest_frames_updated_event: asyncio.Event
134
135
 
@@ -146,7 +147,7 @@ class MediaStreamMixTrack(MediaStreamTrack, Generic[FrameT]):
146
147
  mixer_output_interval: float = 1 / 30,
147
148
  ) -> None:
148
149
  self.kind = kind
149
- self._mixer_callback = mixer_callback
150
+ self._mixer_callback: MixerCallback[FrameT] = mixer_callback
150
151
  self._mixer_callback_lock = threading.Lock()
151
152
 
152
153
  self.mixer_output_interval = mixer_output_interval
@@ -210,7 +211,7 @@ class MediaStreamMixTrack(MediaStreamTrack, Generic[FrameT]):
210
211
 
211
212
  relay = get_global_relay()
212
213
  with loop_context(self._loop):
213
- input_proxy = relay.subscribe(input_track)
214
+ input_proxy = cast(RelayStreamTrack, relay.subscribe(input_track))
214
215
 
215
216
  self._input_proxies[input_track] = input_proxy
216
217
 
@@ -223,7 +224,7 @@ class MediaStreamMixTrack(MediaStreamTrack, Generic[FrameT]):
223
224
  )
224
225
  self._input_tasks[input_proxy] = task
225
226
 
226
- input_proxy.on("ended")(functools.partial(self.remove_input_proxy, input_proxy))
227
+ input_proxy.on("ended", functools.partial(self.remove_input_proxy, input_proxy))
227
228
 
228
229
  def remove_input_proxy(self, input_proxy: RelayStreamTrack) -> None:
229
230
  LOGGER.debug("Remove a relay track %s from %s", input_proxy, self)
@@ -241,13 +242,13 @@ class MediaStreamMixTrack(MediaStreamTrack, Generic[FrameT]):
241
242
  task.cancel()
242
243
 
243
244
  def _set_latest_frame(
244
- self, input_proxy: RelayStreamTrack, frame: Union[Frame, None]
245
+ self, input_proxy: RelayStreamTrack, frame: Union[Frame, Packet, None]
245
246
  ):
246
247
  # TODO: Lock here to make these 2 lines atomic
247
248
  self._latest_frames_map[input_proxy] = frame
248
249
  self._latest_frames_updated_event.set()
249
250
 
250
- async def _get_latest_frames(self) -> List[Frame]:
251
+ async def _get_latest_frames(self) -> List[Union[Frame, Packet]]:
251
252
  # TODO: Lock here to make these 2 lines atomic
252
253
  await self._latest_frames_updated_event.wait()
253
254
  self._latest_frames_updated_event.clear()
@@ -257,8 +258,7 @@ class MediaStreamMixTrack(MediaStreamTrack, Generic[FrameT]):
257
258
  self._latest_frames_map.get(proxy)
258
259
  for proxy in self._input_proxies.values()
259
260
  ]
260
- latest_frames = [f for f in latest_frames if f is not None]
261
- return latest_frames
261
+ return [f for f in latest_frames if f is not None]
262
262
 
263
263
  async def recv(self):
264
264
  if self.readyState != "live":
@@ -25,11 +25,12 @@ class MediaProcessTrack(MediaStreamTrack, Generic[ProcessorT, FrameT]):
25
25
  self.track = track
26
26
  self.processor: ProcessorT = processor
27
27
 
28
- @self.track.on("ended")
29
28
  def on_input_track_ended():
30
29
  logger.debug("Input track %s ended. Stop self %s", self.track, self)
31
30
  self.stop()
32
31
 
32
+ self.track.on("ended", on_input_track_ended)
33
+
33
34
  async def recv(self):
34
35
  if self.readyState != "live":
35
36
  raise MediaStreamError
@@ -98,11 +99,12 @@ class AsyncMediaProcessTrack(MediaStreamTrack, Generic[ProcessorT, FrameT]):
98
99
  )
99
100
  self._thread.start()
100
101
 
101
- @self.track.on("ended")
102
102
  def on_input_track_ended():
103
103
  logger.debug("Input track %s ended. Stop self %s", self.track, self)
104
104
  self.stop()
105
105
 
106
+ self.track.on("ended", on_input_track_ended)
107
+
106
108
  def _run_worker_thread(self):
107
109
  try:
108
110
  self._worker_thread()
@@ -114,7 +116,7 @@ class AsyncMediaProcessTrack(MediaStreamTrack, Generic[ProcessorT, FrameT]):
114
116
  for tbline in tb.rstrip().splitlines():
115
117
  logger.error(tbline.rstrip())
116
118
 
117
- async def _fallback_recv_queued(self, frames: List[FrameT]) -> FrameT:
119
+ async def _fallback_recv_queued(self, frames: List[FrameT]) -> List[FrameT]:
118
120
  """
119
121
  Used as a fallback when the processor does not have its own `recv_queued`.
120
122
  """
@@ -126,7 +128,7 @@ class AsyncMediaProcessTrack(MediaStreamTrack, Generic[ProcessorT, FrameT]):
126
128
  if self.processor.recv:
127
129
  return [self.processor.recv(frames[-1])]
128
130
 
129
- return frames[-1]
131
+ return [frames[-1]]
130
132
 
131
133
  def _worker_thread(self) -> None:
132
134
  loop = asyncio.new_event_loop()
@@ -93,7 +93,7 @@ async def _process_offer_coro(
93
93
 
94
94
  if mode == WebRtcMode.SENDRECV:
95
95
 
96
- @pc.on("track")
96
+ @pc.on("track") # type: ignore
97
97
  def on_track(input_track: MediaStreamTrack):
98
98
  logger.info("Track %s received", input_track.kind)
99
99
 
@@ -164,7 +164,7 @@ async def _process_offer_coro(
164
164
  elif output_track.kind == "audio":
165
165
  on_track_created("output:audio", output_track)
166
166
 
167
- @input_track.on("ended")
167
+ @input_track.on("ended") # type: ignore
168
168
  async def on_ended():
169
169
  logger.info("Track %s ended", input_track.kind)
170
170
  if in_recorder:
@@ -174,7 +174,7 @@ async def _process_offer_coro(
174
174
 
175
175
  elif mode == WebRtcMode.SENDONLY:
176
176
 
177
- @pc.on("track")
177
+ @pc.on("track") # type: ignore
178
178
  def on_track(input_track: MediaStreamTrack):
179
179
  logger.info("Track %s received", input_track.kind)
180
180
 
@@ -183,6 +183,8 @@ async def _process_offer_coro(
183
183
  elif input_track.kind == "audio":
184
184
  on_track_created("input:audio", input_track)
185
185
 
186
+ output_track: MediaStreamTrack
187
+
186
188
  if input_track.kind == "audio":
187
189
  if audio_receiver:
188
190
  if audio_processor:
@@ -224,7 +226,7 @@ async def _process_offer_coro(
224
226
  logger.info("Track %s is added to in_recorder", input_track.kind)
225
227
  in_recorder.addTrack(relay.subscribe(input_track))
226
228
 
227
- @input_track.on("ended")
229
+ @input_track.on("ended") # type: ignore
228
230
  async def on_ended():
229
231
  logger.info("Track %s ended", input_track.kind)
230
232
  if video_receiver:
@@ -237,7 +239,7 @@ async def _process_offer_coro(
237
239
  await pc.setRemoteDescription(offer)
238
240
  if mode == WebRtcMode.RECVONLY:
239
241
  for t in pc.getTransceivers():
240
- output_track = None
242
+ output_track: Optional[MediaStreamTrack] = None
241
243
  if t.kind == "audio":
242
244
  if source_audio_track:
243
245
  if audio_processor:
@@ -393,8 +395,8 @@ class WebRtcWorker(Generic[VideoProcessorT, AudioProcessorT]):
393
395
  self._output_video_track: Optional[MediaStreamTrack] = None
394
396
  self._output_audio_track: Optional[MediaStreamTrack] = None
395
397
  self._player: Optional[MediaPlayer] = None
396
- self._relayed_source_video_track: Optional[MediaRelay] = None
397
- self._relayed_source_audio_track: Optional[MediaRelay] = None
398
+ self._relayed_source_video_track: Optional[MediaStreamTrack] = None
399
+ self._relayed_source_audio_track: Optional[MediaStreamTrack] = None
398
400
 
399
401
  self._session_shutdown_observer = SessionShutdownObserver(self.stop)
400
402
 
@@ -510,7 +512,7 @@ class WebRtcWorker(Generic[VideoProcessorT, AudioProcessorT]):
510
512
  )
511
513
  source_video_track = self._relayed_source_video_track
512
514
 
513
- @self.pc.on("iceconnectionstatechange")
515
+ @self.pc.on("iceconnectionstatechange") # type: ignore
514
516
  async def on_iceconnectionstatechange():
515
517
  logger.info("ICE connection state is %s", self.pc.iceConnectionState)
516
518
  iceConnectionState = self.pc.iceConnectionState
@@ -1,45 +0,0 @@
1
- [tool.poetry]
2
- name = "streamlit-webrtc"
3
- version = "0.49.4"
4
- description = "Real-time video and audio processing on Streamlit"
5
- authors = ["Yuichiro Tachibana (Tsuchiya) <t.yic.yt@gmail.com>"]
6
- license = "MIT"
7
- readme = "README.md"
8
- repository = "https://github.com/whitphx/streamlit-webrtc"
9
- include = [
10
- { path = "./streamlit_webrtc/frontend/dist/**/*", format = ["sdist", "wheel"] }
11
- ]
12
- exclude = [
13
- "./streamlit_webrtc/frontend/*",
14
- ]
15
-
16
- [tool.poetry.dependencies]
17
- python = ">=3.9,<4,!=3.9.7" # 3.9.7 is excluded due to https://github.com/streamlit/streamlit/pull/5168
18
- # For allow-same-origin, >=0.73.0 is required. See https://blog.streamlit.io/streamlit-components-security-and-a-five-month-quest-to-ship-a-single-line-of-code/
19
- # 0.84.0 has an error at marshalling component values.
20
- streamlit = ">=0.84.1"
21
- aiortc = "^1.9.0" # aiortc<1.4.0 causes an error with cryptography>=39.0.0. See https://github.com/whitphx/streamlit-webrtc/issues/1164. The fix was introduced into aiortc in https://github.com/aiortc/aiortc/commit/08b0a7e9f5030a9f7e5617382e92560d4ae763a2 that 1.4.0 included.
22
- packaging = ">=20.0"
23
-
24
- [tool.poetry.group.dev.dependencies]
25
- click = ">=7.0,<9.0" # We must specify the click version because we install older versions of Streamlit in the CI process and their depending click version is different from the newer releases.
26
- ruff = "^0.9.10"
27
- mypy = {extras = ["faster-cache"], version = "^1.15.0"}
28
- pydub = "^0.25.1"
29
- matplotlib = "^3.5.1"
30
- deepspeech = { version = "^0.9.3", python = ">=3.5,<=3.9" } # DeepSpeech 0.9.3 supports Python version 3.5, 3.6, 3.7, 3.8 and 3.9; https://github.com/mozilla/DeepSpeech/releases/tag/v0.9.3
31
- streamlit-server-state = ">=0.17.1,<0.19.0"
32
- pytest = ">=7.1.2,<9.0.0"
33
- opencv-python-headless = "^4.5.4.58"
34
- # It is necessary to specify numpy and pandas versions for Poetry to resolve the package versions correctly and avoid errors.
35
- numpy = "^1.21.0"
36
- pandas = "^2.0.3"
37
- streamlit = "^1.13.0"
38
- twilio = ">=8.1,<10.0"
39
-
40
- [tool.ruff.lint]
41
- extend-select = ["I"]
42
-
43
- [build-system]
44
- requires = ["poetry-core>=1.0.0"]
45
- build-backend = "poetry.core.masonry.api"