py-tgcalls 2.0.6__py3-none-any.whl → 2.1.0.dev2__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.
Files changed (44) hide show
  1. {py_tgcalls-2.0.6.dist-info → py_tgcalls-2.1.0.dev2.dist-info}/METADATA +6 -8
  2. {py_tgcalls-2.0.6.dist-info → py_tgcalls-2.1.0.dev2.dist-info}/RECORD +41 -35
  3. {py_tgcalls-2.0.6.dist-info → py_tgcalls-2.1.0.dev2.dist-info}/WHEEL +1 -1
  4. pytgcalls/__init__.py +2 -0
  5. pytgcalls/__version__.py +1 -1
  6. pytgcalls/filters.py +46 -6
  7. pytgcalls/media_devices/__init__.py +6 -2
  8. pytgcalls/media_devices/device_info.py +8 -15
  9. pytgcalls/media_devices/input_device.py +11 -0
  10. pytgcalls/media_devices/media_devices.py +41 -92
  11. pytgcalls/media_devices/screen_device.py +10 -0
  12. pytgcalls/media_devices/speaker_device.py +10 -0
  13. pytgcalls/methods/stream/__init__.py +4 -2
  14. pytgcalls/methods/stream/play.py +73 -11
  15. pytgcalls/methods/stream/record.py +41 -0
  16. pytgcalls/methods/stream/{played_time.py → time.py} +5 -3
  17. pytgcalls/methods/utilities/call_holder.py +5 -2
  18. pytgcalls/methods/utilities/start.py +118 -13
  19. pytgcalls/methods/utilities/stream_params.py +63 -22
  20. pytgcalls/mtproto/bridged_client.py +34 -2
  21. pytgcalls/mtproto/client_cache.py +46 -16
  22. pytgcalls/mtproto/hydrogram_client.py +53 -11
  23. pytgcalls/mtproto/mtproto_client.py +30 -4
  24. pytgcalls/mtproto/pyrogram_client.py +53 -11
  25. pytgcalls/mtproto/telethon_client.py +61 -19
  26. pytgcalls/scaffold.py +6 -0
  27. pytgcalls/types/__init__.py +10 -4
  28. pytgcalls/types/calls/call.py +5 -3
  29. pytgcalls/types/chats/group_call_participant.py +21 -0
  30. pytgcalls/types/raw/audio_stream.py +3 -3
  31. pytgcalls/types/raw/stream.py +8 -4
  32. pytgcalls/types/raw/video_stream.py +5 -4
  33. pytgcalls/types/stream/__init__.py +10 -4
  34. pytgcalls/types/stream/device.py +24 -0
  35. pytgcalls/types/stream/direction.py +18 -0
  36. pytgcalls/types/stream/media_stream.py +126 -102
  37. pytgcalls/types/stream/record_stream.py +93 -0
  38. pytgcalls/types/stream/stream_ended.py +32 -0
  39. pytgcalls/types/stream/stream_frame.py +35 -0
  40. pytgcalls/media_devices/screen_info.py +0 -45
  41. pytgcalls/types/stream/stream_audio_ended.py +0 -9
  42. pytgcalls/types/stream/stream_video_ended.py +0 -9
  43. {py_tgcalls-2.0.6.dist-info → py_tgcalls-2.1.0.dev2.dist-info}/LICENSE +0 -0
  44. {py_tgcalls-2.0.6.dist-info → py_tgcalls-2.1.0.dev2.dist-info}/top_level.txt +0 -0
@@ -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.change_stream(
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
- call_params: str = await self._binding.create_call(
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
- call_params,
96
+ payload,
90
97
  config.invite_hash,
91
- media_description.video is None,
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
- data.g_a_or_b = await self._binding.create_p2p_call(
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
- self._binding.stop(chat_id)
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
- (py_logger.warning if retries >= 1 else py_logger.info)(
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 PlayedTime(Scaffold):
12
+ class Time(Scaffold):
12
13
  @statictypes
13
14
  @mtproto_required
14
- async def played_time(
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.PLAYING: Call.Status.PLAYING,
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(x, self._conversions[calls_list[x]])
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 StreamAudioEnded
22
- from ...types import StreamVideoEnded
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
+ elif user_id in self._videos_id:
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
+ elif user_id in self._presentations_id:
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(chat_id: int, stream: StreamType):
206
+ async def stream_ended(
207
+ chat_id: int,
208
+ stream_type: StreamType,
209
+ device: StreamDevice,
210
+ ):
150
211
  await self.propagate(
151
- StreamAudioEnded(
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 connection_changed(chat_id: int, state: ConnectionState):
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, stream: asyncio.run_coroutine_threadsafe(
213
- stream_ended(chat_id, stream),
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: asyncio.run_coroutine_threadsafe(
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, state: asyncio.run_coroutine_threadsafe(
225
- connection_changed(chat_id, state),
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.stream import Stream
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(stream: Optional[Stream]) -> MediaDescription:
14
- audio_description = None
15
- video_description = None
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
- if stream.stream_audio is not None:
22
- audio_description = AudioDescription(
23
- input_mode=stream.stream_audio.input_mode,
24
- input=stream.stream_audio.path,
25
- sample_rate=stream.stream_audio.parameters.bitrate,
26
- bits_per_sample=16,
27
- channel_count=stream.stream_audio.parameters.channels,
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
- if stream.stream_video is not None:
31
- video_description = VideoDescription(
32
- input_mode=stream.stream_video.input_mode,
33
- input=stream.stream_video.path,
34
- width=stream.stream_video.parameters.width,
35
- height=stream.stream_video.parameters.height,
36
- fps=stream.stream_video.parameters.frame_rate,
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
- audio=audio_description,
41
- video=video_description,
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
- paused_status: Optional[bool],
101
- stopped_status: Optional[bool],
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
+ [ssrc for ssrc in 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