py-tgcalls 2.0.5__py3-none-any.whl → 2.1.0__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 (65) hide show
  1. {py_tgcalls-2.0.5.dist-info → py_tgcalls-2.1.0.dist-info}/METADATA +11 -16
  2. py_tgcalls-2.1.0.dist-info/RECORD +106 -0
  3. {py_tgcalls-2.0.5.dist-info → py_tgcalls-2.1.0.dist-info}/WHEEL +1 -1
  4. pytgcalls/__init__.py +2 -0
  5. pytgcalls/__version__.py +1 -1
  6. pytgcalls/exceptions.py +7 -24
  7. pytgcalls/ffmpeg.py +2 -2
  8. pytgcalls/filters.py +52 -7
  9. pytgcalls/handlers/handlers_holder.py +1 -1
  10. pytgcalls/media_devices/__init__.py +6 -2
  11. pytgcalls/media_devices/device_info.py +8 -15
  12. pytgcalls/media_devices/input_device.py +11 -0
  13. pytgcalls/media_devices/media_devices.py +41 -92
  14. pytgcalls/media_devices/screen_device.py +10 -0
  15. pytgcalls/media_devices/speaker_device.py +10 -0
  16. pytgcalls/methods/calls/change_volume_call.py +3 -0
  17. pytgcalls/methods/calls/get_participants.py +5 -1
  18. pytgcalls/methods/calls/leave_call.py +3 -1
  19. pytgcalls/methods/stream/__init__.py +14 -10
  20. pytgcalls/methods/stream/{mute_stream.py → mute.py} +2 -2
  21. pytgcalls/methods/stream/{pause_stream.py → pause.py} +2 -2
  22. pytgcalls/methods/stream/play.py +27 -21
  23. pytgcalls/methods/stream/record.py +49 -0
  24. pytgcalls/methods/stream/{resume_stream.py → resume.py} +2 -2
  25. pytgcalls/methods/stream/send_frame.py +38 -0
  26. pytgcalls/methods/stream/{played_time.py → time.py} +5 -3
  27. pytgcalls/methods/stream/{unmute_stream.py → unmute.py} +2 -2
  28. pytgcalls/methods/utilities/__init__.py +6 -0
  29. pytgcalls/methods/utilities/call_holder.py +5 -2
  30. pytgcalls/methods/utilities/join_presentation.py +50 -0
  31. pytgcalls/methods/utilities/log_retries.py +14 -0
  32. pytgcalls/methods/utilities/start.py +136 -16
  33. pytgcalls/methods/utilities/stream_params.py +69 -22
  34. pytgcalls/methods/utilities/update_sources.py +42 -0
  35. pytgcalls/mtproto/bridged_client.py +36 -2
  36. pytgcalls/mtproto/client_cache.py +46 -16
  37. pytgcalls/mtproto/hydrogram_client.py +72 -23
  38. pytgcalls/mtproto/mtproto_client.py +32 -4
  39. pytgcalls/mtproto/pyrogram_client.py +72 -23
  40. pytgcalls/mtproto/telethon_client.py +97 -33
  41. pytgcalls/scaffold.py +15 -0
  42. pytgcalls/types/__init__.py +14 -4
  43. pytgcalls/types/calls/__init__.py +2 -0
  44. pytgcalls/types/calls/call.py +5 -3
  45. pytgcalls/types/calls/call_sources.py +4 -0
  46. pytgcalls/types/chats/group_call_participant.py +23 -0
  47. pytgcalls/types/py_object.py +9 -10
  48. pytgcalls/types/raw/audio_stream.py +3 -3
  49. pytgcalls/types/raw/stream.py +8 -4
  50. pytgcalls/types/raw/video_stream.py +5 -4
  51. pytgcalls/types/stream/__init__.py +14 -4
  52. pytgcalls/types/stream/device.py +36 -0
  53. pytgcalls/types/stream/direction.py +25 -0
  54. pytgcalls/types/stream/external_media.py +8 -0
  55. pytgcalls/types/stream/frame.py +26 -0
  56. pytgcalls/types/stream/media_stream.py +178 -107
  57. pytgcalls/types/stream/record_stream.py +99 -0
  58. pytgcalls/types/stream/stream_ended.py +32 -0
  59. pytgcalls/types/stream/stream_frames.py +20 -0
  60. py_tgcalls-2.0.5.dist-info/RECORD +0 -93
  61. pytgcalls/media_devices/screen_info.py +0 -45
  62. pytgcalls/types/stream/stream_audio_ended.py +0 -9
  63. pytgcalls/types/stream/stream_video_ended.py +0 -9
  64. {py_tgcalls-2.0.5.dist-info → py_tgcalls-2.1.0.dist-info}/LICENSE +0 -0
  65. {py_tgcalls-2.0.5.dist-info → py_tgcalls-2.1.0.dist-info}/top_level.txt +0 -0
@@ -1,17 +1,21 @@
1
- from .mute_stream import MuteStream
2
- from .pause_stream import PauseStream
1
+ from .mute import Mute
2
+ from .pause import Pause
3
3
  from .play import Play
4
- from .played_time import PlayedTime
5
- from .resume_stream import ResumeStream
6
- from .unmute_stream import UnMuteStream
4
+ from .record import Record
5
+ from .resume import Resume
6
+ from .send_frame import SendFrame
7
+ from .time import Time
8
+ from .unmute import UnMute
7
9
 
8
10
 
9
11
  class StreamMethods(
10
- MuteStream,
11
- PauseStream,
12
+ Mute,
13
+ Pause,
12
14
  Play,
13
- PlayedTime,
14
- ResumeStream,
15
- UnMuteStream,
15
+ Record,
16
+ SendFrame,
17
+ Time,
18
+ Resume,
19
+ UnMute,
16
20
  ):
17
21
  pass
@@ -8,10 +8,10 @@ from ...scaffold import Scaffold
8
8
  from ...statictypes import statictypes
9
9
 
10
10
 
11
- class MuteStream(Scaffold):
11
+ class Mute(Scaffold):
12
12
  @statictypes
13
13
  @mtproto_required
14
- async def mute_stream(
14
+ async def mute(
15
15
  self,
16
16
  chat_id: Union[int, str],
17
17
  ):
@@ -8,10 +8,10 @@ from ...scaffold import Scaffold
8
8
  from ...statictypes import statictypes
9
9
 
10
10
 
11
- class PauseStream(Scaffold):
11
+ class Pause(Scaffold):
12
12
  @statictypes
13
13
  @mtproto_required
14
- async def pause_stream(
14
+ async def pause(
15
15
  self,
16
16
  chat_id: Union[int, str],
17
17
  ):
@@ -1,17 +1,19 @@
1
1
  import asyncio
2
2
  import logging
3
+ from pathlib import Path
3
4
  from typing import Optional
4
5
  from typing import Union
5
6
 
6
7
  from ntgcalls import ConnectionNotFound
7
8
  from ntgcalls import FileError
8
- from ntgcalls import InvalidParams
9
+ from ntgcalls import StreamMode
9
10
  from ntgcalls import TelegramServerError
11
+ from ntgcalls import TransportParseException
10
12
 
11
13
  from ...exceptions import NoActiveGroupCall
12
14
  from ...exceptions import TimedOutAnswer
13
15
  from ...exceptions import UnMuteNeeded
14
- from ...mtproto import BridgedClient
16
+ from ...media_devices.input_device import InputDevice
15
17
  from ...mtproto_required import mtproto_required
16
18
  from ...mutex import mutex
17
19
  from ...scaffold import Scaffold
@@ -33,7 +35,7 @@ class Play(Scaffold):
33
35
  async def play(
34
36
  self,
35
37
  chat_id: Union[int, str],
36
- stream: Optional[Stream] = None,
38
+ stream: Optional[Union[str, Path, InputDevice, Stream]] = None,
37
39
  config: Optional[Union[CallConfig, GroupCallConfig]] = None,
38
40
  ):
39
41
  chat_id = await self.resolve_chat_id(chat_id)
@@ -50,8 +52,9 @@ class Play(Scaffold):
50
52
 
51
53
  if chat_id in await self._binding.calls():
52
54
  try:
53
- return await self._binding.change_stream(
55
+ return await self._binding.set_stream_sources(
54
56
  chat_id,
57
+ StreamMode.CAPTURE,
55
58
  media_description,
56
59
  )
57
60
  except FileError as e:
@@ -80,31 +83,36 @@ class Play(Scaffold):
80
83
  try:
81
84
  self._wait_connect[chat_id] = self.loop.create_future()
82
85
  if isinstance(config, GroupCallConfig):
83
- call_params: str = await self._binding.create_call(
86
+ payload: str = await self._binding.create_call(
84
87
  chat_id,
85
88
  media_description,
86
89
  )
87
90
  result_params = await self._app.join_group_call(
88
91
  chat_id,
89
- call_params,
92
+ payload,
90
93
  config.invite_hash,
91
- media_description.video is None,
94
+ media_description.camera is None and
95
+ media_description.screen is None,
92
96
  self._cache_user_peer.get(chat_id),
93
97
  )
94
98
  await self._binding.connect(
95
99
  chat_id,
96
100
  result_params,
101
+ False,
97
102
  )
98
103
  else:
99
104
  data = self._p2p_configs.setdefault(
100
105
  chat_id,
101
106
  CallData(await self._app.get_dhc(), self.loop),
102
107
  )
103
- data.g_a_or_b = await self._binding.create_p2p_call(
108
+ await self._binding.create_p2p_call(
109
+ chat_id,
110
+ media_description,
111
+ )
112
+ data.g_a_or_b = await self._binding.init_exchange(
104
113
  chat_id,
105
114
  data.dh_config,
106
115
  data.g_a_or_b,
107
- media_description,
108
116
  )
109
117
  if not data.outgoing:
110
118
  await self._app.accept_call(
@@ -143,7 +151,11 @@ class Play(Scaffold):
143
151
  result.protocol.p2p_allowed,
144
152
  )
145
153
  except asyncio.TimeoutError:
146
- self._binding.stop(chat_id)
154
+ try:
155
+ await self._binding.stop(chat_id)
156
+ except ConnectionNotFound:
157
+ pass
158
+ await self._app.discard_call(chat_id, True)
147
159
  raise TimedOutAnswer()
148
160
  finally:
149
161
  self._p2p_configs.pop(chat_id, None)
@@ -152,10 +164,7 @@ class Play(Scaffold):
152
164
  except TelegramServerError:
153
165
  if retries == 3 or is_p2p:
154
166
  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
- )
167
+ self._log_retries(retries)
159
168
  except Exception:
160
169
  try:
161
170
  await self._binding.stop(chat_id)
@@ -166,17 +175,14 @@ class Play(Scaffold):
166
175
  self._wait_connect.pop(chat_id, None)
167
176
 
168
177
  if isinstance(config, GroupCallConfig):
169
- participants = await self._app.get_group_call_participants(
178
+ await self._join_presentation(
170
179
  chat_id,
180
+ media_description.screen is not None,
171
181
  )
172
- for x in participants:
173
- if x.user_id == BridgedClient.chat_id(
174
- self._cache_local_peer,
175
- ) and x.muted_by_admin:
176
- self._need_unmute.add(chat_id)
182
+ await self._update_sources(chat_id)
177
183
  except FileError as e:
178
184
  raise FileNotFoundError(e)
179
- except InvalidParams:
185
+ except TransportParseException:
180
186
  raise UnMuteNeeded()
181
187
  except Exception:
182
188
  if isinstance(config, GroupCallConfig):
@@ -0,0 +1,49 @@
1
+ import logging
2
+ from pathlib import Path
3
+ from typing import Optional
4
+ from typing import Union
5
+
6
+ from ntgcalls import FileError
7
+ from ntgcalls import StreamMode
8
+
9
+ from ...media_devices import SpeakerDevice
10
+ from ...mtproto_required import mtproto_required
11
+ from ...scaffold import Scaffold
12
+ from ...statictypes import statictypes
13
+ from ...types import CallConfig
14
+ from ...types import GroupCallConfig
15
+ from ...types.raw import Stream
16
+ from ..utilities.stream_params import StreamParams
17
+
18
+ py_logger = logging.getLogger('pytgcalls')
19
+
20
+
21
+ class Record(Scaffold):
22
+ @statictypes
23
+ @mtproto_required
24
+ async def record(
25
+ self,
26
+ chat_id: Union[int, str],
27
+ stream: Optional[Union[str, Path, Stream, SpeakerDevice]] = None,
28
+ config: Optional[Union[CallConfig, GroupCallConfig]] = None,
29
+ ):
30
+ chat_id = await self.resolve_chat_id(chat_id)
31
+ media_description = await StreamParams.get_record_params(
32
+ stream,
33
+ )
34
+ if chat_id not in await self._binding.calls():
35
+ await self.play(chat_id, config=config)
36
+ try:
37
+ await self._binding.set_stream_sources(
38
+ chat_id,
39
+ StreamMode.PLAYBACK,
40
+ media_description,
41
+ )
42
+ if isinstance(chat_id, int) and chat_id < 0:
43
+ await self._join_presentation(
44
+ chat_id,
45
+ media_description.screen is not None,
46
+ )
47
+ await self._update_sources(chat_id)
48
+ except FileError as e:
49
+ raise FileNotFoundError(e)
@@ -8,10 +8,10 @@ from ...scaffold import Scaffold
8
8
  from ...statictypes import statictypes
9
9
 
10
10
 
11
- class ResumeStream(Scaffold):
11
+ class Resume(Scaffold):
12
12
  @statictypes
13
13
  @mtproto_required
14
- async def resume_stream(
14
+ async def resume(
15
15
  self,
16
16
  chat_id: Union[int, str],
17
17
  ):
@@ -0,0 +1,38 @@
1
+ from typing import Union
2
+
3
+ from ntgcalls import ConnectionNotFound
4
+ from ntgcalls import FrameData
5
+
6
+ from ...exceptions import NotInCallError
7
+ from ...mtproto_required import mtproto_required
8
+ from ...scaffold import Scaffold
9
+ from ...statictypes import statictypes
10
+ from ...types import Device
11
+ from ...types import Frame
12
+
13
+
14
+ class SendFrame(Scaffold):
15
+ @statictypes
16
+ @mtproto_required
17
+ async def send_frame(
18
+ self,
19
+ chat_id: Union[int, str],
20
+ device: Device,
21
+ data: bytes,
22
+ frame_data: Frame.Info = Frame.Info(),
23
+ ):
24
+ chat_id = await self.resolve_chat_id(chat_id)
25
+ try:
26
+ return await self._binding.send_external_frame(
27
+ chat_id,
28
+ Device.to_raw(device),
29
+ data,
30
+ FrameData(
31
+ frame_data.capture_time,
32
+ frame_data.rotation,
33
+ frame_data.width,
34
+ frame_data.height,
35
+ ),
36
+ )
37
+ except ConnectionNotFound:
38
+ raise NotInCallError()
@@ -6,17 +6,19 @@ from ...exceptions import NotInCallError
6
6
  from ...mtproto_required import mtproto_required
7
7
  from ...scaffold import Scaffold
8
8
  from ...statictypes import statictypes
9
+ from ...types import Direction
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
+ direction: Direction = Direction.OUTGOING,
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, direction.to_raw())
21
23
  except ConnectionNotFound:
22
24
  raise NotInCallError()
@@ -8,10 +8,10 @@ from ...scaffold import Scaffold
8
8
  from ...statictypes import statictypes
9
9
 
10
10
 
11
- class UnMuteStream(Scaffold):
11
+ class UnMute(Scaffold):
12
12
  @statictypes
13
13
  @mtproto_required
14
- async def unmute_stream(
14
+ async def unmute(
15
15
  self,
16
16
  chat_id: Union[int, str],
17
17
  ):
@@ -1,19 +1,25 @@
1
1
  from .cache_peer import CachePeer
2
2
  from .call_holder import CallHolder
3
3
  from .cpu_usage import CpuUsage
4
+ from .join_presentation import JoinPresentation
5
+ from .log_retries import LogRetries
4
6
  from .ping import Ping
5
7
  from .resolve_chat_id import ResolveChatID
6
8
  from .run import Run
7
9
  from .start import Start
10
+ from .update_sources import UpdateSources
8
11
 
9
12
 
10
13
  class Utilities(
11
14
  CachePeer,
12
15
  CallHolder,
13
16
  CpuUsage,
17
+ JoinPresentation,
18
+ LogRetries,
14
19
  Ping,
15
20
  ResolveChatID,
16
21
  Run,
17
22
  Start,
23
+ UpdateSources,
18
24
  ):
19
25
  pass
@@ -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
 
@@ -0,0 +1,50 @@
1
+ from typing import Union
2
+
3
+ from ntgcalls import ConnectionError
4
+ from ntgcalls import TelegramServerError
5
+
6
+ from ...scaffold import Scaffold
7
+
8
+
9
+ class JoinPresentation(Scaffold):
10
+ async def _join_presentation(
11
+ self,
12
+ chat_id: Union[int, str],
13
+ join: bool,
14
+ ):
15
+ if join:
16
+ if chat_id in self._presentations:
17
+ return
18
+ for retries in range(4):
19
+ try:
20
+ self._wait_connect[
21
+ chat_id
22
+ ] = self.loop.create_future()
23
+ payload = await self._binding.init_presentation(
24
+ chat_id,
25
+ )
26
+ result_params = await self._app.join_presentation(
27
+ chat_id,
28
+ payload,
29
+ )
30
+ await self._binding.connect(
31
+ chat_id,
32
+ result_params,
33
+ True,
34
+ )
35
+ await self._wait_connect[chat_id]
36
+ self._presentations.add(chat_id)
37
+ break
38
+ except TelegramServerError:
39
+ if retries == 3:
40
+ raise
41
+ self._log_retries(retries)
42
+ finally:
43
+ self._wait_connect.pop(chat_id, None)
44
+ elif chat_id in self._presentations:
45
+ try:
46
+ await self._binding.stop_presentation(chat_id)
47
+ await self._app.leave_presentation(chat_id)
48
+ except ConnectionError:
49
+ pass
50
+ self._presentations.discard(chat_id)
@@ -0,0 +1,14 @@
1
+ import logging
2
+
3
+ from ...scaffold import Scaffold
4
+
5
+ py_logger = logging.getLogger('pytgcalls')
6
+
7
+
8
+ class LogRetries(Scaffold):
9
+ @staticmethod
10
+ def _log_retries(r: int):
11
+ (py_logger.warning if r >= 1 else py_logger.info)(
12
+ f'Telegram is having some internal server issues. '
13
+ f'Retrying {r + 1} of 3',
14
+ )
@@ -1,10 +1,15 @@
1
1
  import asyncio
2
2
  import logging
3
+ from typing import List
3
4
 
4
5
  from ntgcalls import ConnectionError
5
6
  from ntgcalls import ConnectionNotFound
6
7
  from ntgcalls import ConnectionState
8
+ from ntgcalls import Frame as RawFrame
7
9
  from ntgcalls import MediaState
10
+ from ntgcalls import NetworkInfo
11
+ from ntgcalls import StreamDevice
12
+ from ntgcalls import StreamMode
8
13
  from ntgcalls import StreamType
9
14
  from ntgcalls import TelegramServerError
10
15
 
@@ -16,10 +21,13 @@ from ...pytgcalls_session import PyTgCallsSession
16
21
  from ...scaffold import Scaffold
17
22
  from ...types import CallData
18
23
  from ...types import ChatUpdate
24
+ from ...types import Device
25
+ from ...types import Direction
26
+ from ...types import Frame
19
27
  from ...types import GroupCallParticipant
20
28
  from ...types import RawCallUpdate
21
- from ...types import StreamAudioEnded
22
- from ...types import StreamVideoEnded
29
+ from ...types import StreamEnded
30
+ from ...types import StreamFrames
23
31
  from ...types import Update
24
32
  from ...types import UpdatedGroupCallParticipant
25
33
 
@@ -43,6 +51,7 @@ class Start(Scaffold):
43
51
  if isinstance(update, ChatUpdate) and \
44
52
  p2p_config.outgoing:
45
53
  if update.status & ChatUpdate.Status.DISCARDED_CALL:
54
+ self._wait_connect.pop(chat_id, None)
46
55
  p2p_config.wait_data.set_exception(
47
56
  CallDeclined(
48
57
  chat_id,
@@ -84,6 +93,68 @@ class Start(Scaffold):
84
93
  participant = update.participant
85
94
  action = participant.action
86
95
  chat_peer = self._cache_user_peer.get(chat_id)
96
+ user_id = participant.user_id
97
+ if chat_id in self._call_sources:
98
+ call_sources = self._call_sources[chat_id]
99
+ was_camera = user_id in call_sources.camera
100
+ was_screen = user_id in call_sources.presentation
101
+
102
+ if was_camera != participant.video_camera:
103
+ if participant.video_info:
104
+ self._call_sources[chat_id].camera[
105
+ user_id
106
+ ] = participant.video_info.endpoint
107
+ try:
108
+ await self._binding.add_incoming_video(
109
+ chat_id,
110
+ participant.video_info.endpoint,
111
+ participant.video_info.sources,
112
+ )
113
+ except (ConnectionNotFound, ConnectionError):
114
+ pass
115
+ elif user_id in self._call_sources[chat_id].camera:
116
+ try:
117
+ await self._binding.remove_incoming_video(
118
+ chat_id,
119
+ self._call_sources[
120
+ chat_id
121
+ ].camera[user_id],
122
+ )
123
+ except (ConnectionNotFound, ConnectionError):
124
+ pass
125
+ self._call_sources[chat_id].camera.pop(
126
+ user_id, None,
127
+ )
128
+
129
+ if was_screen != participant.screen_sharing:
130
+ if participant.presentation_info:
131
+ self._call_sources[chat_id].presentation[
132
+ user_id
133
+ ] = participant.presentation_info.endpoint
134
+ try:
135
+ await self._binding.add_incoming_video(
136
+ chat_id,
137
+ participant.presentation_info.endpoint,
138
+ participant.presentation_info.sources,
139
+ )
140
+ except (ConnectionNotFound, ConnectionError):
141
+ pass
142
+ elif user_id in self._call_sources[
143
+ chat_id
144
+ ].presentation:
145
+ try:
146
+ await self._binding.remove_incoming_video(
147
+ chat_id,
148
+ self._call_sources[
149
+ chat_id
150
+ ].presentation[user_id],
151
+ )
152
+ except (ConnectionNotFound, ConnectionError):
153
+ pass
154
+ self._call_sources[chat_id].presentation.pop(
155
+ user_id, None,
156
+ )
157
+
87
158
  if chat_peer:
88
159
  is_self = BridgedClient.chat_id(
89
160
  chat_peer,
@@ -91,7 +162,7 @@ class Start(Scaffold):
91
162
  if is_self:
92
163
  if action == GroupCallParticipant.Action.LEFT:
93
164
  if await clear_call(chat_id):
94
- await self.propagate(
165
+ await self._propagate(
95
166
  ChatUpdate(
96
167
  chat_id,
97
168
  ChatUpdate.Status.KICKED,
@@ -119,7 +190,7 @@ class Start(Scaffold):
119
190
  else:
120
191
  self._need_unmute.discard(chat_id)
121
192
  if not isinstance(update, RawCallUpdate):
122
- await self.propagate(
193
+ await self._propagate(
123
194
  update,
124
195
  self,
125
196
  )
@@ -141,18 +212,22 @@ class Start(Scaffold):
141
212
  state.muted,
142
213
  state.video_paused,
143
214
  state.video_stopped,
215
+ state.presentation_paused,
144
216
  self._cache_user_peer.get(chat_id),
145
217
  )
146
218
  except Exception as e:
147
219
  py_logger.debug(f'SetVideoCallStatus: {e}')
148
220
 
149
- async def stream_ended(chat_id: int, stream: StreamType):
150
- await self.propagate(
151
- StreamAudioEnded(
152
- chat_id,
153
- ) if stream == StreamType.AUDIO else
154
- StreamVideoEnded(
221
+ async def stream_ended(
222
+ chat_id: int,
223
+ stream_type: StreamType,
224
+ device: StreamDevice,
225
+ ):
226
+ await self._propagate(
227
+ StreamEnded(
155
228
  chat_id,
229
+ StreamEnded.Type.from_raw(stream_type),
230
+ Device.from_raw(device),
156
231
  ),
157
232
  self,
158
233
  )
@@ -166,7 +241,38 @@ class Start(Scaffold):
166
241
  except (ConnectionError, ConnectionNotFound):
167
242
  pass
168
243
 
169
- async def connection_changed(chat_id: int, state: ConnectionState):
244
+ async def stream_frame(
245
+ chat_id: int,
246
+ mode: StreamMode,
247
+ device: StreamDevice,
248
+ frames: List[RawFrame],
249
+ ):
250
+ await self._propagate(
251
+ StreamFrames(
252
+ chat_id,
253
+ Direction.from_raw(mode),
254
+ Device.from_raw(device),
255
+ [
256
+ Frame(
257
+ x.ssrc,
258
+ x.data,
259
+ Frame.Info(
260
+ x.frame_data.absolute_capture_timestamp_ms,
261
+ x.frame_data.width,
262
+ x.frame_data.height,
263
+ x.frame_data.rotation,
264
+ ),
265
+ ) for x in frames
266
+ ],
267
+ ),
268
+ self,
269
+ )
270
+
271
+ async def connection_changed(
272
+ chat_id: int,
273
+ net_state: NetworkInfo,
274
+ ):
275
+ state = net_state.state
170
276
  if state == ConnectionState.CONNECTING:
171
277
  return
172
278
  if chat_id in self._wait_connect:
@@ -209,20 +315,22 @@ class Start(Scaffold):
209
315
  self._handle_mtproto()
210
316
 
211
317
  self._binding.on_stream_end(
212
- lambda chat_id, stream: asyncio.run_coroutine_threadsafe(
213
- stream_ended(chat_id, stream),
318
+ lambda chat_id, stream_type, device:
319
+ asyncio.run_coroutine_threadsafe(
320
+ stream_ended(chat_id, stream_type, device),
214
321
  self.loop,
215
322
  ),
216
323
  )
217
324
  self._binding.on_upgrade(
218
- lambda chat_id, state: asyncio.run_coroutine_threadsafe(
325
+ lambda chat_id, state:
326
+ asyncio.run_coroutine_threadsafe(
219
327
  update_status(chat_id, state),
220
328
  self.loop,
221
329
  ),
222
330
  )
223
331
  self._binding.on_connection_change(
224
- lambda chat_id, state: asyncio.run_coroutine_threadsafe(
225
- connection_changed(chat_id, state),
332
+ lambda chat_id, net_state: asyncio.run_coroutine_threadsafe(
333
+ connection_changed(chat_id, net_state),
226
334
  self.loop,
227
335
  ),
228
336
  )
@@ -232,6 +340,18 @@ class Start(Scaffold):
232
340
  self.loop,
233
341
  ),
234
342
  )
343
+ self._binding.on_frames(
344
+ lambda chat_id, mode, device, frames:
345
+ asyncio.run_coroutine_threadsafe(
346
+ stream_frame(
347
+ chat_id,
348
+ mode,
349
+ device,
350
+ frames,
351
+ ),
352
+ self.loop,
353
+ ),
354
+ )
235
355
  await PyTgCallsSession().start()
236
356
  else:
237
357
  raise PyTgCallsAlreadyRunning()