1# This Source Code Form is subject to the terms of the Mozilla Public
2# License, v. 2.0. If a copy of the MPL was not distributed with this
3# file, You can obtain one at https://mozilla.org/MPL/2.0/.
4#
5# This file incorporates work covered by the following copyright and
6# permission notice:
7#
8# Copyright (c) Jeremy Lainé.
9# All rights reserved.
10#
11# Redistribution and use in source and binary forms, with or without
12# modification, are permitted provided that the following conditions are met:
13#
14# * Redistributions of source code must retain the above copyright notice,
15# this list of conditions and the following disclaimer.
16# * Redistributions in binary form must reproduce the above copyright notice,
17# this list of conditions and the following disclaimer in the documentation
18# and/or other materials provided with the distribution.
19# * Neither the name of aiortc nor the names of its contributors may
20# be used to endorse or promote products derived from this software without
21# specific prior written permission.
22#
23# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
24# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
25# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
26# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
27# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
28# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
29# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
30# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
31# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
32# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33
34import logging
35from dataclasses import dataclass
36from typing import Optional, Union
37
38from pyee.asyncio import AsyncIOEventEmitter
39
40from .exceptions import InvalidStateError
41
42logger = logging.getLogger(__name__)
43
44
45@dataclass
46class RTCDataChannelParameters:
47 """
48 The :class:`RTCDataChannelParameters` dictionary describes the
49 configuration of an :class:`RTCDataChannel`.
50 """
51
52 label: str = ""
53 "A name describing the data channel."
54
55 maxPacketLifeTime: Optional[int] = None
56 "The maximum time in milliseconds during which transmissions are attempted."
57
58 maxRetransmits: Optional[int] = None
59 "The maximum number of retransmissions that are attempted."
60
61 ordered: bool = True
62 "Whether the data channel guarantees in-order delivery of messages."
63
64 protocol: str = ""
65 "The name of the subprotocol in use."
66
67 negotiated: bool = False
68 """
69 Whether data channel will be negotiated out of-band, where both sides
70 create data channel with an agreed-upon ID."""
71
72 id: Optional[int] = None
73 """
74 An numeric ID for the channel; permitted values are 0-65534.
75 If you don't include this option, the user agent will select an ID for you.
76 Must be set when negotiating out-of-band.
77 """
78
79
80class RTCDataChannel(AsyncIOEventEmitter):
81 """
82 The :class:`RTCDataChannel` interface represents a network channel which
83 can be used for bidirectional peer-to-peer transfers of arbitrary data.
84
85 :param transport: An :class:`RTCSctpTransport`.
86 :param parameters: An :class:`RTCDataChannelParameters`.
87 """
88
89 def __init__(
90 self,
91 transport: "RTCSctpTransport",
92 parameters: RTCDataChannelParameters,
93 send_open: bool = True,
94 ) -> None:
95 super().__init__()
96 self.__bufferedAmount = 0
97 self.__bufferedAmountLowThreshold = 0
98 self.__id = parameters.id
99 self.__parameters = parameters
100 self.__readyState = "connecting"
101 self.__transport = transport
102 self.__send_open = send_open
103
104 if self.__parameters.negotiated and (
105 self.__id is None or self.__id < 0 or self.__id > 65534
106 ):
107 raise ValueError(
108 "ID must be in range 0-65534 if data channel is negotiated out-of-band"
109 )
110
111 if not self.__parameters.negotiated:
112 if self.__send_open:
113 self.__send_open = False
114 self.__transport._data_channel_open(self)
115 else:
116 self.__transport._data_channel_add_negotiated(self)
117
118 @property
119 def bufferedAmount(self) -> int:
120 """
121 The number of bytes of data currently queued to be sent over the data channel.
122 """
123 return self.__bufferedAmount
124
125 @property
126 def bufferedAmountLowThreshold(self) -> int:
127 """
128 The number of bytes of buffered outgoing data that is considered "low".
129 """
130 return self.__bufferedAmountLowThreshold
131
132 @bufferedAmountLowThreshold.setter
133 def bufferedAmountLowThreshold(self, value: int) -> None:
134 if value < 0 or value > 4294967295:
135 raise ValueError(
136 "bufferedAmountLowThreshold must be in range 0 - 4294967295"
137 )
138 self.__bufferedAmountLowThreshold = value
139
140 @property
141 def negotiated(self) -> bool:
142 """
143 Whether data channel was negotiated out-of-band.
144 """
145 return self.__parameters.negotiated
146
147 @property
148 def id(self) -> Optional[int]:
149 """
150 An ID number which uniquely identifies the data channel.
151 """
152 return self.__id
153
154 @property
155 def label(self) -> str:
156 """
157 A name describing the data channel.
158
159 These labels are not required to be unique.
160 """
161 return self.__parameters.label
162
163 @property
164 def ordered(self) -> bool:
165 """
166 Indicates whether or not the data channel guarantees in-order delivery of
167 messages.
168 """
169 return self.__parameters.ordered
170
171 @property
172 def maxPacketLifeTime(self) -> Optional[int]:
173 """
174 The maximum time in milliseconds during which transmissions are attempted.
175 """
176 return self.__parameters.maxPacketLifeTime
177
178 @property
179 def maxRetransmits(self) -> Optional[int]:
180 """
181 "The maximum number of retransmissions that are attempted.
182 """
183 return self.__parameters.maxRetransmits
184
185 @property
186 def protocol(self) -> str:
187 """
188 The name of the subprotocol in use.
189 """
190 return self.__parameters.protocol
191
192 @property
193 def readyState(self) -> str:
194 """
195 A string indicating the current state of the underlying data transport.
196 """
197 return self.__readyState
198
199 @property
200 def transport(self) -> "RTCSctpTransport":
201 """
202 The :class:`RTCSctpTransport` over which data is transmitted.
203 """
204 return self.__transport
205
206 def close(self) -> None:
207 """
208 Close the data channel.
209 """
210 self.transport._data_channel_close(self)
211
212 def send(self, data: Union[bytes, str]) -> None:
213 """
214 Send `data` across the data channel to the remote peer.
215 """
216 if self.readyState != "open":
217 raise InvalidStateError
218
219 if not isinstance(data, (str, bytes)):
220 raise ValueError(f"Cannot send unsupported data type: {type(data)}")
221
222 self.transport._data_channel_send(self, data)
223
224 def _addBufferedAmount(self, amount: int) -> None:
225 crosses_threshold = (
226 self.__bufferedAmount > self.bufferedAmountLowThreshold
227 and self.__bufferedAmount + amount <= self.bufferedAmountLowThreshold
228 )
229 self.__bufferedAmount += amount
230 if crosses_threshold:
231 self.emit("bufferedamountlow")
232
233 def _setId(self, id: int) -> None:
234 self.__id = id
235
236 def _setReadyState(self, state: str) -> None:
237 if state != self.__readyState:
238 self.__log_debug("- %s -> %s", self.__readyState, state)
239 self.__readyState = state
240
241 if state == "open":
242 self.emit("open")
243 elif state == "closed":
244 self.emit("close")
245
246 # no more events will be emitted, so remove all event listeners
247 # to facilitate garbage collection.
248 self.remove_all_listeners()
249
250 def __log_debug(self, msg: str, *args: object) -> None:
251 logger.debug(f"RTCDataChannel(%s) {msg}", self.__id, *args)
252
253
254from .rtcsctptransport import RTCSctpTransport # noqa