py-tgcalls 2.0.5__py3-none-any.whl → 2.1.0.dev1__py3-none-any.whl
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.
- {py_tgcalls-2.0.5.dist-info → py_tgcalls-2.1.0.dev1.dist-info}/METADATA +6 -8
- {py_tgcalls-2.0.5.dist-info → py_tgcalls-2.1.0.dev1.dist-info}/RECORD +41 -35
- {py_tgcalls-2.0.5.dist-info → py_tgcalls-2.1.0.dev1.dist-info}/WHEEL +1 -1
- pytgcalls/__init__.py +2 -0
- pytgcalls/__version__.py +1 -1
- pytgcalls/filters.py +49 -8
- pytgcalls/media_devices/__init__.py +6 -2
- pytgcalls/media_devices/device_info.py +8 -15
- pytgcalls/media_devices/input_device.py +11 -0
- pytgcalls/media_devices/media_devices.py +41 -92
- pytgcalls/media_devices/screen_device.py +10 -0
- pytgcalls/media_devices/speaker_device.py +10 -0
- pytgcalls/methods/stream/__init__.py +4 -2
- pytgcalls/methods/stream/play.py +73 -11
- pytgcalls/methods/stream/record.py +41 -0
- pytgcalls/methods/stream/{played_time.py → time.py} +5 -3
- pytgcalls/methods/utilities/call_holder.py +5 -2
- pytgcalls/methods/utilities/start.py +118 -13
- pytgcalls/methods/utilities/stream_params.py +63 -22
- pytgcalls/mtproto/bridged_client.py +34 -2
- pytgcalls/mtproto/client_cache.py +46 -16
- pytgcalls/mtproto/hydrogram_client.py +53 -11
- pytgcalls/mtproto/mtproto_client.py +30 -4
- pytgcalls/mtproto/pyrogram_client.py +53 -11
- pytgcalls/mtproto/telethon_client.py +61 -19
- pytgcalls/scaffold.py +6 -0
- pytgcalls/types/__init__.py +10 -4
- pytgcalls/types/calls/call.py +5 -3
- pytgcalls/types/chats/group_call_participant.py +21 -0
- pytgcalls/types/raw/audio_stream.py +3 -3
- pytgcalls/types/raw/stream.py +8 -4
- pytgcalls/types/raw/video_stream.py +3 -3
- pytgcalls/types/stream/__init__.py +10 -4
- pytgcalls/types/stream/device.py +24 -0
- pytgcalls/types/stream/direction.py +18 -0
- pytgcalls/types/stream/media_stream.py +35 -29
- pytgcalls/types/stream/record_stream.py +93 -0
- pytgcalls/types/stream/stream_ended.py +32 -0
- pytgcalls/types/stream/stream_frame.py +35 -0
- pytgcalls/media_devices/screen_info.py +0 -45
- pytgcalls/types/stream/stream_audio_ended.py +0 -9
- pytgcalls/types/stream/stream_video_ended.py +0 -9
- {py_tgcalls-2.0.5.dist-info → py_tgcalls-2.1.0.dev1.dist-info}/LICENSE +0 -0
- {py_tgcalls-2.0.5.dist-info → py_tgcalls-2.1.0.dev1.dist-info}/top_level.txt +0 -0
pytgcalls/methods/stream/play.py
CHANGED
|
@@ -6,6 +6,7 @@ from typing import Union
|
|
|
6
6
|
from ntgcalls import ConnectionNotFound
|
|
7
7
|
from ntgcalls import FileError
|
|
8
8
|
from ntgcalls import InvalidParams
|
|
9
|
+
from ntgcalls import StreamMode
|
|
9
10
|
from ntgcalls import TelegramServerError
|
|
10
11
|
|
|
11
12
|
from ...exceptions import NoActiveGroupCall
|
|
@@ -36,6 +37,11 @@ class Play(Scaffold):
|
|
|
36
37
|
stream: Optional[Stream] = None,
|
|
37
38
|
config: Optional[Union[CallConfig, GroupCallConfig]] = None,
|
|
38
39
|
):
|
|
40
|
+
def log_retries(r: int):
|
|
41
|
+
(py_logger.warning if r >= 1 else py_logger.info)(
|
|
42
|
+
f'Telegram is having some internal server issues. '
|
|
43
|
+
f'Retrying {r + 1} of 3',
|
|
44
|
+
)
|
|
39
45
|
chat_id = await self.resolve_chat_id(chat_id)
|
|
40
46
|
is_p2p = chat_id > 0 # type: ignore
|
|
41
47
|
if config is None:
|
|
@@ -50,8 +56,9 @@ class Play(Scaffold):
|
|
|
50
56
|
|
|
51
57
|
if chat_id in await self._binding.calls():
|
|
52
58
|
try:
|
|
53
|
-
return await self._binding.
|
|
59
|
+
return await self._binding.set_stream_sources(
|
|
54
60
|
chat_id,
|
|
61
|
+
StreamMode.CAPTURE,
|
|
55
62
|
media_description,
|
|
56
63
|
)
|
|
57
64
|
except FileError as e:
|
|
@@ -80,31 +87,36 @@ class Play(Scaffold):
|
|
|
80
87
|
try:
|
|
81
88
|
self._wait_connect[chat_id] = self.loop.create_future()
|
|
82
89
|
if isinstance(config, GroupCallConfig):
|
|
83
|
-
|
|
90
|
+
payload: str = await self._binding.create_call(
|
|
84
91
|
chat_id,
|
|
85
92
|
media_description,
|
|
86
93
|
)
|
|
87
94
|
result_params = await self._app.join_group_call(
|
|
88
95
|
chat_id,
|
|
89
|
-
|
|
96
|
+
payload,
|
|
90
97
|
config.invite_hash,
|
|
91
|
-
media_description.
|
|
98
|
+
media_description.camera is None and
|
|
99
|
+
media_description.screen is None,
|
|
92
100
|
self._cache_user_peer.get(chat_id),
|
|
93
101
|
)
|
|
94
102
|
await self._binding.connect(
|
|
95
103
|
chat_id,
|
|
96
104
|
result_params,
|
|
105
|
+
False,
|
|
97
106
|
)
|
|
98
107
|
else:
|
|
99
108
|
data = self._p2p_configs.setdefault(
|
|
100
109
|
chat_id,
|
|
101
110
|
CallData(await self._app.get_dhc(), self.loop),
|
|
102
111
|
)
|
|
103
|
-
|
|
112
|
+
await self._binding.create_p2p_call(
|
|
113
|
+
chat_id,
|
|
114
|
+
media_description,
|
|
115
|
+
)
|
|
116
|
+
data.g_a_or_b = await self._binding.init_exchange(
|
|
104
117
|
chat_id,
|
|
105
118
|
data.dh_config,
|
|
106
119
|
data.g_a_or_b,
|
|
107
|
-
media_description,
|
|
108
120
|
)
|
|
109
121
|
if not data.outgoing:
|
|
110
122
|
await self._app.accept_call(
|
|
@@ -143,7 +155,10 @@ class Play(Scaffold):
|
|
|
143
155
|
result.protocol.p2p_allowed,
|
|
144
156
|
)
|
|
145
157
|
except asyncio.TimeoutError:
|
|
146
|
-
|
|
158
|
+
try:
|
|
159
|
+
await self._binding.stop(chat_id)
|
|
160
|
+
except ConnectionNotFound:
|
|
161
|
+
pass
|
|
147
162
|
raise TimedOutAnswer()
|
|
148
163
|
finally:
|
|
149
164
|
self._p2p_configs.pop(chat_id, None)
|
|
@@ -152,10 +167,7 @@ class Play(Scaffold):
|
|
|
152
167
|
except TelegramServerError:
|
|
153
168
|
if retries == 3 or is_p2p:
|
|
154
169
|
raise
|
|
155
|
-
(
|
|
156
|
-
f'Telegram is having some internal server issues. '
|
|
157
|
-
f'Retrying {retries + 1} of 3',
|
|
158
|
-
)
|
|
170
|
+
log_retries(retries)
|
|
159
171
|
except Exception:
|
|
160
172
|
try:
|
|
161
173
|
await self._binding.stop(chat_id)
|
|
@@ -165,11 +177,61 @@ class Play(Scaffold):
|
|
|
165
177
|
finally:
|
|
166
178
|
self._wait_connect.pop(chat_id, None)
|
|
167
179
|
|
|
180
|
+
if isinstance(config, GroupCallConfig):
|
|
181
|
+
if media_description.screen is not None:
|
|
182
|
+
for retries in range(4):
|
|
183
|
+
try:
|
|
184
|
+
self._wait_connect[
|
|
185
|
+
chat_id
|
|
186
|
+
] = self.loop.create_future()
|
|
187
|
+
payload = await self._binding.init_presentation(
|
|
188
|
+
chat_id,
|
|
189
|
+
)
|
|
190
|
+
result_params = await self._app.join_presentation(
|
|
191
|
+
chat_id,
|
|
192
|
+
payload,
|
|
193
|
+
)
|
|
194
|
+
await self._binding.connect(
|
|
195
|
+
chat_id,
|
|
196
|
+
result_params,
|
|
197
|
+
True,
|
|
198
|
+
)
|
|
199
|
+
await self._wait_connect[chat_id]
|
|
200
|
+
self._presentations.add(chat_id)
|
|
201
|
+
except TelegramServerError:
|
|
202
|
+
if retries == 3:
|
|
203
|
+
raise
|
|
204
|
+
log_retries(retries)
|
|
205
|
+
finally:
|
|
206
|
+
self._wait_connect.pop(chat_id, None)
|
|
207
|
+
elif chat_id in self._presentations:
|
|
208
|
+
await self._binding.stop_presentation(chat_id)
|
|
209
|
+
await self._app.leave_presentation(chat_id)
|
|
210
|
+
self._presentations.discard(chat_id)
|
|
211
|
+
|
|
168
212
|
if isinstance(config, GroupCallConfig):
|
|
169
213
|
participants = await self._app.get_group_call_participants(
|
|
170
214
|
chat_id,
|
|
171
215
|
)
|
|
172
216
|
for x in participants:
|
|
217
|
+
if x.video_info is not None:
|
|
218
|
+
self._videos_id[
|
|
219
|
+
chat_id
|
|
220
|
+
] = x.video_info.endpoint
|
|
221
|
+
self._binding.add_incoming_video(
|
|
222
|
+
chat_id,
|
|
223
|
+
x.video_info.endpoint,
|
|
224
|
+
x.video_info.sources,
|
|
225
|
+
)
|
|
226
|
+
if x.presentation_info is not None:
|
|
227
|
+
self._presentations_id[
|
|
228
|
+
chat_id
|
|
229
|
+
] = x.presentation_info.endpoint
|
|
230
|
+
self._binding.add_incoming_video(
|
|
231
|
+
chat_id,
|
|
232
|
+
x.presentation_info.endpoint,
|
|
233
|
+
x.presentation_info.sources,
|
|
234
|
+
)
|
|
173
235
|
if x.user_id == BridgedClient.chat_id(
|
|
174
236
|
self._cache_local_peer,
|
|
175
237
|
) and x.muted_by_admin:
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from typing import Union
|
|
4
|
+
|
|
5
|
+
from ntgcalls import FileError
|
|
6
|
+
from ntgcalls import StreamMode
|
|
7
|
+
|
|
8
|
+
from ...mtproto_required import mtproto_required
|
|
9
|
+
from ...scaffold import Scaffold
|
|
10
|
+
from ...statictypes import statictypes
|
|
11
|
+
from ...types import CallConfig
|
|
12
|
+
from ...types import GroupCallConfig
|
|
13
|
+
from ...types.raw import Stream
|
|
14
|
+
from ..utilities.stream_params import StreamParams
|
|
15
|
+
|
|
16
|
+
py_logger = logging.getLogger('pytgcalls')
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Record(Scaffold):
|
|
20
|
+
@statictypes
|
|
21
|
+
@mtproto_required
|
|
22
|
+
async def record(
|
|
23
|
+
self,
|
|
24
|
+
chat_id: Union[int, str],
|
|
25
|
+
stream: Optional[Stream] = None,
|
|
26
|
+
config: Optional[Union[CallConfig, GroupCallConfig]] = None,
|
|
27
|
+
):
|
|
28
|
+
chat_id = await self.resolve_chat_id(chat_id)
|
|
29
|
+
media_description = await StreamParams.get_record_params(
|
|
30
|
+
stream,
|
|
31
|
+
)
|
|
32
|
+
if chat_id not in await self._binding.calls():
|
|
33
|
+
await self.play(chat_id, config=config)
|
|
34
|
+
try:
|
|
35
|
+
return await self._binding.set_stream_sources(
|
|
36
|
+
chat_id,
|
|
37
|
+
StreamMode.PLAYBACK,
|
|
38
|
+
media_description,
|
|
39
|
+
)
|
|
40
|
+
except FileError as e:
|
|
41
|
+
raise FileNotFoundError(e)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from typing import Union
|
|
2
2
|
|
|
3
3
|
from ntgcalls import ConnectionNotFound
|
|
4
|
+
from ntgcalls import StreamMode
|
|
4
5
|
|
|
5
6
|
from ...exceptions import NotInCallError
|
|
6
7
|
from ...mtproto_required import mtproto_required
|
|
@@ -8,15 +9,16 @@ from ...scaffold import Scaffold
|
|
|
8
9
|
from ...statictypes import statictypes
|
|
9
10
|
|
|
10
11
|
|
|
11
|
-
class
|
|
12
|
+
class Time(Scaffold):
|
|
12
13
|
@statictypes
|
|
13
14
|
@mtproto_required
|
|
14
|
-
async def
|
|
15
|
+
async def time(
|
|
15
16
|
self,
|
|
16
17
|
chat_id: Union[int, str],
|
|
18
|
+
stream_mode: StreamMode = StreamMode.CAPTURE,
|
|
17
19
|
):
|
|
18
20
|
chat_id = await self.resolve_chat_id(chat_id)
|
|
19
21
|
try:
|
|
20
|
-
return await self._binding.time(chat_id)
|
|
22
|
+
return await self._binding.time(chat_id, stream_mode)
|
|
21
23
|
except ConnectionNotFound:
|
|
22
24
|
raise NotInCallError()
|
|
@@ -9,7 +9,7 @@ class CallHolder(Scaffold):
|
|
|
9
9
|
def __init__(self):
|
|
10
10
|
super().__init__()
|
|
11
11
|
self._conversions = {
|
|
12
|
-
StreamStatus.
|
|
12
|
+
StreamStatus.ACTIVE: Call.Status.ACTIVE,
|
|
13
13
|
StreamStatus.PAUSED: Call.Status.PAUSED,
|
|
14
14
|
StreamStatus.IDLING: Call.Status.IDLE,
|
|
15
15
|
}
|
|
@@ -18,7 +18,10 @@ class CallHolder(Scaffold):
|
|
|
18
18
|
async def calls(self):
|
|
19
19
|
calls_list = await self._binding.calls()
|
|
20
20
|
return Dict({
|
|
21
|
-
x: Call(
|
|
21
|
+
x: Call(
|
|
22
|
+
x, self._conversions[calls_list[x].playback],
|
|
23
|
+
self._conversions[calls_list[x].capture],
|
|
24
|
+
)
|
|
22
25
|
for x in calls_list
|
|
23
26
|
})
|
|
24
27
|
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import logging
|
|
3
3
|
|
|
4
|
+
from ntgcalls import CallNetworkState
|
|
4
5
|
from ntgcalls import ConnectionError
|
|
5
6
|
from ntgcalls import ConnectionNotFound
|
|
6
7
|
from ntgcalls import ConnectionState
|
|
8
|
+
from ntgcalls import FrameData
|
|
7
9
|
from ntgcalls import MediaState
|
|
10
|
+
from ntgcalls import StreamDevice
|
|
11
|
+
from ntgcalls import StreamMode
|
|
8
12
|
from ntgcalls import StreamType
|
|
9
13
|
from ntgcalls import TelegramServerError
|
|
10
14
|
|
|
@@ -16,10 +20,12 @@ from ...pytgcalls_session import PyTgCallsSession
|
|
|
16
20
|
from ...scaffold import Scaffold
|
|
17
21
|
from ...types import CallData
|
|
18
22
|
from ...types import ChatUpdate
|
|
23
|
+
from ...types import Device
|
|
24
|
+
from ...types import Direction
|
|
19
25
|
from ...types import GroupCallParticipant
|
|
20
26
|
from ...types import RawCallUpdate
|
|
21
|
-
from ...types import
|
|
22
|
-
from ...types import
|
|
27
|
+
from ...types import StreamEnded
|
|
28
|
+
from ...types import StreamFrame
|
|
23
29
|
from ...types import Update
|
|
24
30
|
from ...types import UpdatedGroupCallParticipant
|
|
25
31
|
|
|
@@ -84,6 +90,56 @@ class Start(Scaffold):
|
|
|
84
90
|
participant = update.participant
|
|
85
91
|
action = participant.action
|
|
86
92
|
chat_peer = self._cache_user_peer.get(chat_id)
|
|
93
|
+
user_id = participant.user_id
|
|
94
|
+
was_camera = user_id in self._videos_id
|
|
95
|
+
was_screen = user_id in self._presentations_id
|
|
96
|
+
|
|
97
|
+
if was_camera != participant.video_camera:
|
|
98
|
+
if participant.video_info:
|
|
99
|
+
self._videos_id[
|
|
100
|
+
user_id
|
|
101
|
+
] = participant.video_info.endpoint
|
|
102
|
+
try:
|
|
103
|
+
await self._binding.add_incoming_video(
|
|
104
|
+
chat_id,
|
|
105
|
+
participant.video_info.endpoint,
|
|
106
|
+
participant.video_info.sources,
|
|
107
|
+
)
|
|
108
|
+
except ConnectionNotFound:
|
|
109
|
+
pass
|
|
110
|
+
else:
|
|
111
|
+
try:
|
|
112
|
+
await self._binding.remove_incoming_video(
|
|
113
|
+
chat_id,
|
|
114
|
+
self._videos_id[user_id],
|
|
115
|
+
)
|
|
116
|
+
except ConnectionNotFound:
|
|
117
|
+
pass
|
|
118
|
+
self._videos_id.pop(user_id)
|
|
119
|
+
|
|
120
|
+
if was_screen != participant.screen_sharing:
|
|
121
|
+
if participant.presentation_info:
|
|
122
|
+
self._presentations_id[
|
|
123
|
+
user_id
|
|
124
|
+
] = participant.presentation_info.endpoint
|
|
125
|
+
try:
|
|
126
|
+
await self._binding.add_incoming_video(
|
|
127
|
+
chat_id,
|
|
128
|
+
participant.presentation_info.endpoint,
|
|
129
|
+
participant.presentation_info.sources,
|
|
130
|
+
)
|
|
131
|
+
except ConnectionNotFound:
|
|
132
|
+
pass
|
|
133
|
+
else:
|
|
134
|
+
try:
|
|
135
|
+
await self._binding.remove_incoming_video(
|
|
136
|
+
chat_id,
|
|
137
|
+
self._presentations_id[user_id],
|
|
138
|
+
)
|
|
139
|
+
except ConnectionNotFound:
|
|
140
|
+
pass
|
|
141
|
+
self._presentations_id.pop(user_id)
|
|
142
|
+
|
|
87
143
|
if chat_peer:
|
|
88
144
|
is_self = BridgedClient.chat_id(
|
|
89
145
|
chat_peer,
|
|
@@ -141,18 +197,22 @@ class Start(Scaffold):
|
|
|
141
197
|
state.muted,
|
|
142
198
|
state.video_paused,
|
|
143
199
|
state.video_stopped,
|
|
200
|
+
state.presentation_paused,
|
|
144
201
|
self._cache_user_peer.get(chat_id),
|
|
145
202
|
)
|
|
146
203
|
except Exception as e:
|
|
147
204
|
py_logger.debug(f'SetVideoCallStatus: {e}')
|
|
148
205
|
|
|
149
|
-
async def stream_ended(
|
|
206
|
+
async def stream_ended(
|
|
207
|
+
chat_id: int,
|
|
208
|
+
stream_type: StreamType,
|
|
209
|
+
device: StreamDevice,
|
|
210
|
+
):
|
|
150
211
|
await self.propagate(
|
|
151
|
-
|
|
152
|
-
chat_id,
|
|
153
|
-
) if stream == StreamType.AUDIO else
|
|
154
|
-
StreamVideoEnded(
|
|
212
|
+
StreamEnded(
|
|
155
213
|
chat_id,
|
|
214
|
+
StreamEnded.Type.from_raw(stream_type),
|
|
215
|
+
Device.from_raw(device),
|
|
156
216
|
),
|
|
157
217
|
self,
|
|
158
218
|
)
|
|
@@ -166,7 +226,36 @@ class Start(Scaffold):
|
|
|
166
226
|
except (ConnectionError, ConnectionNotFound):
|
|
167
227
|
pass
|
|
168
228
|
|
|
169
|
-
async def
|
|
229
|
+
async def stream_frame(
|
|
230
|
+
chat_id: int,
|
|
231
|
+
source_id: int,
|
|
232
|
+
mode: StreamMode,
|
|
233
|
+
device: StreamDevice,
|
|
234
|
+
frame: bytes,
|
|
235
|
+
frame_info: FrameData,
|
|
236
|
+
):
|
|
237
|
+
await self.propagate(
|
|
238
|
+
StreamFrame(
|
|
239
|
+
chat_id,
|
|
240
|
+
source_id,
|
|
241
|
+
Direction.from_raw(mode),
|
|
242
|
+
Device.from_raw(device),
|
|
243
|
+
frame,
|
|
244
|
+
StreamFrame.Info(
|
|
245
|
+
frame_info.absolute_capture_timestamp_ms,
|
|
246
|
+
frame_info.width,
|
|
247
|
+
frame_info.height,
|
|
248
|
+
frame_info.rotation,
|
|
249
|
+
),
|
|
250
|
+
),
|
|
251
|
+
self,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
async def connection_changed(
|
|
255
|
+
chat_id: int,
|
|
256
|
+
net_state: CallNetworkState,
|
|
257
|
+
):
|
|
258
|
+
state = net_state.connection_state
|
|
170
259
|
if state == ConnectionState.CONNECTING:
|
|
171
260
|
return
|
|
172
261
|
if chat_id in self._wait_connect:
|
|
@@ -209,20 +298,22 @@ class Start(Scaffold):
|
|
|
209
298
|
self._handle_mtproto()
|
|
210
299
|
|
|
211
300
|
self._binding.on_stream_end(
|
|
212
|
-
lambda chat_id,
|
|
213
|
-
|
|
301
|
+
lambda chat_id, stream_type, device:
|
|
302
|
+
asyncio.run_coroutine_threadsafe(
|
|
303
|
+
stream_ended(chat_id, stream_type, device),
|
|
214
304
|
self.loop,
|
|
215
305
|
),
|
|
216
306
|
)
|
|
217
307
|
self._binding.on_upgrade(
|
|
218
|
-
lambda chat_id, state:
|
|
308
|
+
lambda chat_id, state:
|
|
309
|
+
asyncio.run_coroutine_threadsafe(
|
|
219
310
|
update_status(chat_id, state),
|
|
220
311
|
self.loop,
|
|
221
312
|
),
|
|
222
313
|
)
|
|
223
314
|
self._binding.on_connection_change(
|
|
224
|
-
lambda chat_id,
|
|
225
|
-
connection_changed(chat_id,
|
|
315
|
+
lambda chat_id, net_state: asyncio.run_coroutine_threadsafe(
|
|
316
|
+
connection_changed(chat_id, net_state),
|
|
226
317
|
self.loop,
|
|
227
318
|
),
|
|
228
319
|
)
|
|
@@ -232,6 +323,20 @@ class Start(Scaffold):
|
|
|
232
323
|
self.loop,
|
|
233
324
|
),
|
|
234
325
|
)
|
|
326
|
+
self._binding.on_frame(
|
|
327
|
+
lambda chat_id, source_id, mode, device, frame, info:
|
|
328
|
+
asyncio.run_coroutine_threadsafe(
|
|
329
|
+
stream_frame(
|
|
330
|
+
chat_id,
|
|
331
|
+
source_id,
|
|
332
|
+
mode,
|
|
333
|
+
device,
|
|
334
|
+
frame,
|
|
335
|
+
info,
|
|
336
|
+
),
|
|
337
|
+
self.loop,
|
|
338
|
+
),
|
|
339
|
+
)
|
|
235
340
|
await PyTgCallsSession().start()
|
|
236
341
|
else:
|
|
237
342
|
raise PyTgCallsAlreadyRunning()
|
|
@@ -1,42 +1,83 @@
|
|
|
1
1
|
from typing import Optional
|
|
2
|
+
from typing import Union
|
|
2
3
|
|
|
3
4
|
from ntgcalls import AudioDescription
|
|
4
5
|
from ntgcalls import MediaDescription
|
|
5
6
|
from ntgcalls import VideoDescription
|
|
6
7
|
|
|
7
|
-
from ...types.raw
|
|
8
|
+
from ...types.raw import AudioStream
|
|
9
|
+
from ...types.raw import Stream
|
|
10
|
+
from ...types.raw import VideoStream
|
|
8
11
|
from ...types.stream.media_stream import MediaStream
|
|
9
12
|
|
|
10
13
|
|
|
11
14
|
class StreamParams:
|
|
12
15
|
@staticmethod
|
|
13
|
-
async def get_stream_params(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
async def get_stream_params(
|
|
17
|
+
stream: Optional[Stream],
|
|
18
|
+
) -> MediaDescription:
|
|
17
19
|
if stream is not None:
|
|
18
20
|
if isinstance(stream, MediaStream):
|
|
19
21
|
await stream.check_stream()
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
return MediaDescription(
|
|
24
|
+
microphone=StreamParams._parse_media_description(
|
|
25
|
+
None if stream is None else stream.microphone,
|
|
26
|
+
),
|
|
27
|
+
speaker=StreamParams._parse_media_description(
|
|
28
|
+
None if stream is None else stream.speaker,
|
|
29
|
+
),
|
|
30
|
+
camera=StreamParams._parse_media_description(
|
|
31
|
+
None if stream is None else stream.camera,
|
|
32
|
+
),
|
|
33
|
+
screen=StreamParams._parse_media_description(
|
|
34
|
+
None if stream is None else stream.screen,
|
|
35
|
+
),
|
|
36
|
+
)
|
|
29
37
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
38
|
+
@staticmethod
|
|
39
|
+
def _parse_media_description(
|
|
40
|
+
media: Optional[Union[AudioStream, VideoStream]],
|
|
41
|
+
) -> Optional[Union[AudioDescription, VideoDescription]]:
|
|
42
|
+
if media is not None:
|
|
43
|
+
if isinstance(media, AudioStream):
|
|
44
|
+
return AudioDescription(
|
|
45
|
+
media_source=media.media_source,
|
|
46
|
+
input=media.path,
|
|
47
|
+
sample_rate=media.parameters.bitrate,
|
|
48
|
+
channel_count=media.parameters.channels,
|
|
37
49
|
)
|
|
50
|
+
elif isinstance(media, VideoStream):
|
|
51
|
+
return VideoDescription(
|
|
52
|
+
media_source=media.media_source,
|
|
53
|
+
input=media.path,
|
|
54
|
+
width=media.parameters.width,
|
|
55
|
+
height=media.parameters.height,
|
|
56
|
+
fps=media.parameters.frame_rate,
|
|
57
|
+
)
|
|
58
|
+
return None
|
|
38
59
|
|
|
60
|
+
@staticmethod
|
|
61
|
+
async def get_record_params(
|
|
62
|
+
stream: Optional[Stream],
|
|
63
|
+
) -> MediaDescription:
|
|
64
|
+
if stream is not None:
|
|
65
|
+
if isinstance(stream, MediaStream):
|
|
66
|
+
raise ValueError(
|
|
67
|
+
'Stream should be an instance of '
|
|
68
|
+
'RecordStream or a raw Stream',
|
|
69
|
+
)
|
|
39
70
|
return MediaDescription(
|
|
40
|
-
|
|
41
|
-
|
|
71
|
+
microphone=StreamParams._parse_media_description(
|
|
72
|
+
None if stream is None else stream.microphone,
|
|
73
|
+
),
|
|
74
|
+
speaker=StreamParams._parse_media_description(
|
|
75
|
+
None if stream is None else stream.speaker,
|
|
76
|
+
),
|
|
77
|
+
camera=StreamParams._parse_media_description(
|
|
78
|
+
None if stream is None else stream.camera,
|
|
79
|
+
),
|
|
80
|
+
screen=StreamParams._parse_media_description(
|
|
81
|
+
None if stream is None else stream.screen,
|
|
82
|
+
),
|
|
42
83
|
)
|
|
@@ -6,6 +6,7 @@ from typing import Optional
|
|
|
6
6
|
|
|
7
7
|
from ntgcalls import Protocol
|
|
8
8
|
from ntgcalls import RTCServer
|
|
9
|
+
from ntgcalls import SsrcGroup
|
|
9
10
|
|
|
10
11
|
from ..handlers import HandlersHolder
|
|
11
12
|
from ..types import GroupCallParticipant
|
|
@@ -29,6 +30,19 @@ class BridgedClient(HandlersHolder):
|
|
|
29
30
|
):
|
|
30
31
|
pass
|
|
31
32
|
|
|
33
|
+
async def join_presentation(
|
|
34
|
+
self,
|
|
35
|
+
chat_id: int,
|
|
36
|
+
json_join: str,
|
|
37
|
+
):
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
async def leave_presentation(
|
|
41
|
+
self,
|
|
42
|
+
chat_id: int,
|
|
43
|
+
):
|
|
44
|
+
pass
|
|
45
|
+
|
|
32
46
|
async def request_call(
|
|
33
47
|
self,
|
|
34
48
|
user_id: int,
|
|
@@ -97,8 +111,9 @@ class BridgedClient(HandlersHolder):
|
|
|
97
111
|
self,
|
|
98
112
|
chat_id: int,
|
|
99
113
|
muted_status: Optional[bool],
|
|
100
|
-
|
|
101
|
-
|
|
114
|
+
video_paused: Optional[bool],
|
|
115
|
+
video_stopped: Optional[bool],
|
|
116
|
+
presentation_paused: Optional[bool],
|
|
102
117
|
participant: Any,
|
|
103
118
|
):
|
|
104
119
|
pass
|
|
@@ -128,6 +143,21 @@ class BridgedClient(HandlersHolder):
|
|
|
128
143
|
def package_name(obj):
|
|
129
144
|
return str(obj.__class__.__module__).split('.')[0]
|
|
130
145
|
|
|
146
|
+
@staticmethod
|
|
147
|
+
def parse_source(source) -> Optional[GroupCallParticipant.SourceInfo]:
|
|
148
|
+
if not source:
|
|
149
|
+
return None
|
|
150
|
+
return GroupCallParticipant.SourceInfo(
|
|
151
|
+
source.endpoint,
|
|
152
|
+
[
|
|
153
|
+
SsrcGroup(
|
|
154
|
+
source_group.semantics,
|
|
155
|
+
source_group.sources,
|
|
156
|
+
)
|
|
157
|
+
for source_group in source.source_groups
|
|
158
|
+
],
|
|
159
|
+
)
|
|
160
|
+
|
|
131
161
|
@staticmethod
|
|
132
162
|
def parse_participant(participant):
|
|
133
163
|
return GroupCallParticipant(
|
|
@@ -143,6 +173,8 @@ class BridgedClient(HandlersHolder):
|
|
|
143
173
|
if participant.volume is not None else 100,
|
|
144
174
|
bool(participant.just_joined),
|
|
145
175
|
bool(participant.left),
|
|
176
|
+
BridgedClient.parse_source(participant.video),
|
|
177
|
+
BridgedClient.parse_source(participant.presentation),
|
|
146
178
|
)
|
|
147
179
|
|
|
148
180
|
@staticmethod
|