HAP-python 4.9.2__py3-none-any.whl → 5.0.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.
- {HAP_python-4.9.2.dist-info → hap_python-5.0.0.dist-info}/METADATA +16 -16
- hap_python-5.0.0.dist-info/RECORD +29 -0
- {HAP_python-4.9.2.dist-info → hap_python-5.0.0.dist-info}/WHEEL +1 -1
- hap_python-5.0.0.dist-info/licenses/AUTHORS +6 -0
- pyhap/accessory.py +3 -1
- pyhap/accessory_driver.py +8 -3
- pyhap/camera.py +390 -308
- pyhap/characteristic.py +1 -0
- pyhap/const.py +4 -4
- pyhap/encoder.py +1 -0
- pyhap/hap_crypto.py +1 -0
- pyhap/hap_handler.py +11 -5
- pyhap/hap_protocol.py +1 -0
- pyhap/iid_manager.py +1 -0
- pyhap/loader.py +1 -0
- pyhap/state.py +2 -1
- pyhap/tlv.py +2 -1
- pyhap/util.py +9 -3
- HAP_python-4.9.2.dist-info/RECORD +0 -28
- {HAP_python-4.9.2.dist-info → hap_python-5.0.0.dist-info/licenses}/LICENSE +0 -0
- {HAP_python-4.9.2.dist-info → hap_python-5.0.0.dist-info}/top_level.txt +0 -0
pyhap/camera.py
CHANGED
|
@@ -22,205 +22,209 @@ import ipaddress
|
|
|
22
22
|
import logging
|
|
23
23
|
import os
|
|
24
24
|
import struct
|
|
25
|
+
import sys
|
|
25
26
|
from uuid import UUID
|
|
26
27
|
|
|
27
|
-
import async_timeout
|
|
28
|
-
|
|
29
28
|
from pyhap import RESOURCE_DIR, tlv
|
|
30
29
|
from pyhap.accessory import Accessory
|
|
31
30
|
from pyhap.const import CATEGORY_CAMERA
|
|
32
31
|
from pyhap.util import byte_bool, to_base64_str
|
|
33
32
|
|
|
33
|
+
if sys.version_info >= (3, 11):
|
|
34
|
+
from asyncio import timeout as async_timeout
|
|
35
|
+
else:
|
|
36
|
+
from async_timeout import timeout as async_timeout
|
|
37
|
+
|
|
34
38
|
SETUP_TYPES = {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
"SESSION_ID": b"\x01",
|
|
40
|
+
"STATUS": b"\x02",
|
|
41
|
+
"ADDRESS": b"\x03",
|
|
42
|
+
"VIDEO_SRTP_PARAM": b"\x04",
|
|
43
|
+
"AUDIO_SRTP_PARAM": b"\x05",
|
|
44
|
+
"VIDEO_SSRC": b"\x06",
|
|
45
|
+
"AUDIO_SSRC": b"\x07",
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
|
|
45
49
|
SETUP_STATUS = {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
50
|
+
"SUCCESS": b"\x00",
|
|
51
|
+
"BUSY": b"\x01",
|
|
52
|
+
"ERROR": b"\x02",
|
|
49
53
|
}
|
|
50
54
|
|
|
51
55
|
|
|
52
56
|
SETUP_IPV = {
|
|
53
|
-
|
|
54
|
-
|
|
57
|
+
"IPV4": b"\x00",
|
|
58
|
+
"IPV6": b"\x01",
|
|
55
59
|
}
|
|
56
60
|
|
|
57
61
|
|
|
58
62
|
SETUP_ADDR_INFO = {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
"ADDRESS_VER": b"\x01",
|
|
64
|
+
"ADDRESS": b"\x02",
|
|
65
|
+
"VIDEO_RTP_PORT": b"\x03",
|
|
66
|
+
"AUDIO_RTP_PORT": b"\x04",
|
|
63
67
|
}
|
|
64
68
|
|
|
65
69
|
|
|
66
70
|
SETUP_SRTP_PARAM = {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
71
|
+
"CRYPTO": b"\x01",
|
|
72
|
+
"MASTER_KEY": b"\x02",
|
|
73
|
+
"MASTER_SALT": b"\x03",
|
|
70
74
|
}
|
|
71
75
|
|
|
72
76
|
|
|
73
77
|
STREAMING_STATUS = {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
78
|
+
"AVAILABLE": b"\x00",
|
|
79
|
+
"STREAMING": b"\x01",
|
|
80
|
+
"BUSY": b"\x02",
|
|
77
81
|
}
|
|
78
82
|
|
|
79
83
|
|
|
80
84
|
RTP_CONFIG_TYPES = {
|
|
81
|
-
|
|
85
|
+
"CRYPTO": b"\x02",
|
|
82
86
|
}
|
|
83
87
|
|
|
84
88
|
|
|
85
89
|
SRTP_CRYPTO_SUITES = {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
90
|
+
"AES_CM_128_HMAC_SHA1_80": b"\x00",
|
|
91
|
+
"AES_CM_256_HMAC_SHA1_80": b"\x01",
|
|
92
|
+
"NONE": b"\x02",
|
|
89
93
|
}
|
|
90
94
|
|
|
91
95
|
|
|
92
96
|
VIDEO_TYPES = {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
+
"CODEC": b"\x01",
|
|
98
|
+
"CODEC_PARAM": b"\x02",
|
|
99
|
+
"ATTRIBUTES": b"\x03",
|
|
100
|
+
"RTP_PARAM": b"\x04",
|
|
97
101
|
}
|
|
98
102
|
|
|
99
103
|
|
|
100
104
|
VIDEO_CODEC_TYPES = {
|
|
101
|
-
|
|
105
|
+
"H264": b"\x00",
|
|
102
106
|
}
|
|
103
107
|
|
|
104
108
|
|
|
105
109
|
VIDEO_CODEC_PARAM_TYPES = {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
110
|
+
"PROFILE_ID": b"\x01",
|
|
111
|
+
"LEVEL": b"\x02",
|
|
112
|
+
"PACKETIZATION_MODE": b"\x03",
|
|
113
|
+
"CVO_ENABLED": b"\x04",
|
|
114
|
+
"CVO_ID": b"\x05",
|
|
111
115
|
}
|
|
112
116
|
|
|
113
117
|
|
|
114
118
|
VIDEO_CODEC_PARAM_CVO_TYPES = {
|
|
115
|
-
|
|
116
|
-
|
|
119
|
+
"UNSUPPORTED": b"\x01",
|
|
120
|
+
"SUPPORTED": b"\x02",
|
|
117
121
|
}
|
|
118
122
|
|
|
119
123
|
|
|
120
124
|
VIDEO_CODEC_PARAM_PROFILE_ID_TYPES = {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
125
|
+
"BASELINE": b"\x00",
|
|
126
|
+
"MAIN": b"\x01",
|
|
127
|
+
"HIGH": b"\x02",
|
|
124
128
|
}
|
|
125
129
|
|
|
126
130
|
|
|
127
131
|
VIDEO_CODEC_PARAM_LEVEL_TYPES = {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
132
|
+
"TYPE3_1": b"\x00",
|
|
133
|
+
"TYPE3_2": b"\x01",
|
|
134
|
+
"TYPE4_0": b"\x02",
|
|
131
135
|
}
|
|
132
136
|
|
|
133
137
|
|
|
134
138
|
VIDEO_CODEC_PARAM_PACKETIZATION_MODE_TYPES = {
|
|
135
|
-
|
|
139
|
+
"NON_INTERLEAVED": b"\x00",
|
|
136
140
|
}
|
|
137
141
|
|
|
138
142
|
|
|
139
143
|
VIDEO_ATTRIBUTES_TYPES = {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
144
|
+
"IMAGE_WIDTH": b"\x01",
|
|
145
|
+
"IMAGE_HEIGHT": b"\x02",
|
|
146
|
+
"FRAME_RATE": b"\x03",
|
|
143
147
|
}
|
|
144
148
|
|
|
145
149
|
|
|
146
|
-
SUPPORTED_VIDEO_CONFIG_TAG = b
|
|
150
|
+
SUPPORTED_VIDEO_CONFIG_TAG = b"\x01"
|
|
147
151
|
|
|
148
152
|
|
|
149
153
|
SELECTED_STREAM_CONFIGURATION_TYPES = {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
154
|
+
"SESSION": b"\x01",
|
|
155
|
+
"VIDEO": b"\x02",
|
|
156
|
+
"AUDIO": b"\x03",
|
|
153
157
|
}
|
|
154
158
|
|
|
155
159
|
|
|
156
160
|
RTP_PARAM_TYPES = {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
161
|
+
"PAYLOAD_TYPE": b"\x01",
|
|
162
|
+
"SYNCHRONIZATION_SOURCE": b"\x02",
|
|
163
|
+
"MAX_BIT_RATE": b"\x03",
|
|
164
|
+
"RTCP_SEND_INTERVAL": b"\x04",
|
|
165
|
+
"MAX_MTU": b"\x05",
|
|
166
|
+
"COMFORT_NOISE_PAYLOAD_TYPE": b"\x06",
|
|
163
167
|
}
|
|
164
168
|
|
|
165
169
|
|
|
166
170
|
AUDIO_TYPES = {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
+
"CODEC": b"\x01",
|
|
172
|
+
"CODEC_PARAM": b"\x02",
|
|
173
|
+
"RTP_PARAM": b"\x03",
|
|
174
|
+
"COMFORT_NOISE": b"\x04",
|
|
171
175
|
}
|
|
172
176
|
|
|
173
177
|
|
|
174
178
|
AUDIO_CODEC_TYPES = {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
+
"PCMU": b"\x00",
|
|
180
|
+
"PCMA": b"\x01",
|
|
181
|
+
"AACELD": b"\x02",
|
|
182
|
+
"OPUS": b"\x03",
|
|
179
183
|
}
|
|
180
184
|
|
|
181
185
|
|
|
182
186
|
AUDIO_CODEC_PARAM_TYPES = {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
+
"CHANNEL": b"\x01",
|
|
188
|
+
"BIT_RATE": b"\x02",
|
|
189
|
+
"SAMPLE_RATE": b"\x03",
|
|
190
|
+
"PACKET_TIME": b"\x04",
|
|
187
191
|
}
|
|
188
192
|
|
|
189
193
|
|
|
190
194
|
AUDIO_CODEC_PARAM_BIT_RATE_TYPES = {
|
|
191
|
-
|
|
192
|
-
|
|
195
|
+
"VARIABLE": b"\x00",
|
|
196
|
+
"CONSTANT": b"\x01",
|
|
193
197
|
}
|
|
194
198
|
|
|
195
199
|
|
|
196
200
|
AUDIO_CODEC_PARAM_SAMPLE_RATE_TYPES = {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
201
|
+
"KHZ_8": b"\x00",
|
|
202
|
+
"KHZ_16": b"\x01",
|
|
203
|
+
"KHZ_24": b"\x02",
|
|
200
204
|
}
|
|
201
205
|
|
|
202
206
|
|
|
203
|
-
SUPPORTED_AUDIO_CODECS_TAG = b
|
|
204
|
-
SUPPORTED_COMFORT_NOISE_TAG = b
|
|
205
|
-
SUPPORTED_AUDIO_CONFIG_TAG = b
|
|
206
|
-
SET_CONFIG_REQUEST_TAG = b
|
|
207
|
-
SESSION_ID = b
|
|
207
|
+
SUPPORTED_AUDIO_CODECS_TAG = b"\x01"
|
|
208
|
+
SUPPORTED_COMFORT_NOISE_TAG = b"\x02"
|
|
209
|
+
SUPPORTED_AUDIO_CONFIG_TAG = b"\x02"
|
|
210
|
+
SET_CONFIG_REQUEST_TAG = b"\x02"
|
|
211
|
+
SESSION_ID = b"\x01"
|
|
208
212
|
|
|
209
213
|
|
|
210
|
-
NO_SRTP = b
|
|
211
|
-
|
|
214
|
+
NO_SRTP = b"\x01\x01\x02\x02\x00\x03\x00"
|
|
215
|
+
"""Configuration value for no SRTP."""
|
|
212
216
|
|
|
213
217
|
|
|
214
218
|
FFMPEG_CMD = (
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
219
|
+
"ffmpeg -re -f avfoundation -framerate {fps} -i 0:0 -threads 0 "
|
|
220
|
+
"-vcodec libx264 -an -pix_fmt yuv420p -r {fps} -f rawvideo -tune zerolatency "
|
|
221
|
+
"-vf scale={width}:{height} -b:v {v_max_bitrate}k -bufsize {v_max_bitrate}k "
|
|
222
|
+
"-payload_type 99 -ssrc {v_ssrc} -f rtp "
|
|
223
|
+
"-srtp_out_suite AES_CM_128_HMAC_SHA1_80 -srtp_out_params {v_srtp_key} "
|
|
224
|
+
"srtp://{address}:{v_port}?rtcpport={v_port}&"
|
|
225
|
+
"localrtcpport={v_port}&pkt_size=1378"
|
|
222
226
|
)
|
|
223
|
-
|
|
227
|
+
"""Template for the ffmpeg command."""
|
|
224
228
|
|
|
225
229
|
logger = logging.getLogger(__name__)
|
|
226
230
|
|
|
@@ -242,10 +246,10 @@ class Camera(Accessory):
|
|
|
242
246
|
:type support_srtp: bool
|
|
243
247
|
"""
|
|
244
248
|
if support_srtp:
|
|
245
|
-
crypto = SRTP_CRYPTO_SUITES[
|
|
249
|
+
crypto = SRTP_CRYPTO_SUITES["AES_CM_128_HMAC_SHA1_80"]
|
|
246
250
|
else:
|
|
247
|
-
crypto = SRTP_CRYPTO_SUITES[
|
|
248
|
-
return tlv.encode(RTP_CONFIG_TYPES[
|
|
251
|
+
crypto = SRTP_CRYPTO_SUITES["NONE"]
|
|
252
|
+
return tlv.encode(RTP_CONFIG_TYPES["CRYPTO"], crypto, to_base64=True)
|
|
249
253
|
|
|
250
254
|
@staticmethod
|
|
251
255
|
def get_supported_video_stream_config(video_params):
|
|
@@ -259,31 +263,41 @@ class Camera(Accessory):
|
|
|
259
263
|
:type video_params: dict
|
|
260
264
|
"""
|
|
261
265
|
codec_params_tlv = tlv.encode(
|
|
262
|
-
VIDEO_CODEC_PARAM_TYPES[
|
|
263
|
-
VIDEO_CODEC_PARAM_PACKETIZATION_MODE_TYPES[
|
|
266
|
+
VIDEO_CODEC_PARAM_TYPES["PACKETIZATION_MODE"],
|
|
267
|
+
VIDEO_CODEC_PARAM_PACKETIZATION_MODE_TYPES["NON_INTERLEAVED"],
|
|
268
|
+
)
|
|
264
269
|
|
|
265
|
-
codec_params = video_params[
|
|
266
|
-
for profile in codec_params[
|
|
267
|
-
codec_params_tlv +=
|
|
268
|
-
|
|
270
|
+
codec_params = video_params["codec"]
|
|
271
|
+
for profile in codec_params["profiles"]:
|
|
272
|
+
codec_params_tlv += tlv.encode(
|
|
273
|
+
VIDEO_CODEC_PARAM_TYPES["PROFILE_ID"], profile
|
|
274
|
+
)
|
|
269
275
|
|
|
270
|
-
for level in codec_params[
|
|
271
|
-
codec_params_tlv +=
|
|
272
|
-
tlv.encode(VIDEO_CODEC_PARAM_TYPES['LEVEL'], level)
|
|
276
|
+
for level in codec_params["levels"]:
|
|
277
|
+
codec_params_tlv += tlv.encode(VIDEO_CODEC_PARAM_TYPES["LEVEL"], level)
|
|
273
278
|
|
|
274
|
-
attr_tlv = b
|
|
275
|
-
for resolution in video_params[
|
|
279
|
+
attr_tlv = b""
|
|
280
|
+
for resolution in video_params["resolutions"]:
|
|
276
281
|
res_tlv = tlv.encode(
|
|
277
|
-
VIDEO_ATTRIBUTES_TYPES[
|
|
278
|
-
|
|
279
|
-
VIDEO_ATTRIBUTES_TYPES[
|
|
280
|
-
|
|
282
|
+
VIDEO_ATTRIBUTES_TYPES["IMAGE_WIDTH"],
|
|
283
|
+
struct.pack("<H", resolution[0]),
|
|
284
|
+
VIDEO_ATTRIBUTES_TYPES["IMAGE_HEIGHT"],
|
|
285
|
+
struct.pack("<H", resolution[1]),
|
|
286
|
+
VIDEO_ATTRIBUTES_TYPES["FRAME_RATE"],
|
|
287
|
+
struct.pack("<H", resolution[2]),
|
|
288
|
+
)
|
|
289
|
+
attr_tlv += tlv.encode(VIDEO_TYPES["ATTRIBUTES"], res_tlv)
|
|
281
290
|
|
|
282
|
-
config_tlv = tlv.encode(
|
|
283
|
-
|
|
291
|
+
config_tlv = tlv.encode(
|
|
292
|
+
VIDEO_TYPES["CODEC"],
|
|
293
|
+
VIDEO_CODEC_TYPES["H264"],
|
|
294
|
+
VIDEO_TYPES["CODEC_PARAM"],
|
|
295
|
+
codec_params_tlv,
|
|
296
|
+
)
|
|
284
297
|
|
|
285
|
-
return tlv.encode(
|
|
286
|
-
|
|
298
|
+
return tlv.encode(
|
|
299
|
+
SUPPORTED_VIDEO_CONFIG_TAG, config_tlv + attr_tlv, to_base64=True
|
|
300
|
+
)
|
|
287
301
|
|
|
288
302
|
@staticmethod
|
|
289
303
|
def get_supported_audio_stream_config(audio_params):
|
|
@@ -299,60 +313,71 @@ class Camera(Accessory):
|
|
|
299
313
|
:type audio_params: dict
|
|
300
314
|
"""
|
|
301
315
|
has_supported_codec = False
|
|
302
|
-
configs = b
|
|
303
|
-
for codec_param in audio_params[
|
|
304
|
-
param_type = codec_param[
|
|
305
|
-
if param_type ==
|
|
316
|
+
configs = b""
|
|
317
|
+
for codec_param in audio_params["codecs"]:
|
|
318
|
+
param_type = codec_param["type"]
|
|
319
|
+
if param_type == "OPUS":
|
|
306
320
|
has_supported_codec = True
|
|
307
|
-
codec = AUDIO_CODEC_TYPES[
|
|
308
|
-
bitrate = AUDIO_CODEC_PARAM_BIT_RATE_TYPES[
|
|
309
|
-
elif param_type ==
|
|
321
|
+
codec = AUDIO_CODEC_TYPES["OPUS"]
|
|
322
|
+
bitrate = AUDIO_CODEC_PARAM_BIT_RATE_TYPES["VARIABLE"]
|
|
323
|
+
elif param_type == "AAC-eld":
|
|
310
324
|
has_supported_codec = True
|
|
311
|
-
codec = AUDIO_CODEC_TYPES[
|
|
312
|
-
bitrate = AUDIO_CODEC_PARAM_BIT_RATE_TYPES[
|
|
325
|
+
codec = AUDIO_CODEC_TYPES["AACELD"]
|
|
326
|
+
bitrate = AUDIO_CODEC_PARAM_BIT_RATE_TYPES["VARIABLE"]
|
|
313
327
|
else:
|
|
314
|
-
logger.warning(
|
|
328
|
+
logger.warning("Unsupported codec %s", param_type)
|
|
315
329
|
continue
|
|
316
330
|
|
|
317
|
-
param_samplerate = codec_param[
|
|
331
|
+
param_samplerate = codec_param["samplerate"]
|
|
318
332
|
if param_samplerate == 8:
|
|
319
|
-
samplerate = AUDIO_CODEC_PARAM_SAMPLE_RATE_TYPES[
|
|
333
|
+
samplerate = AUDIO_CODEC_PARAM_SAMPLE_RATE_TYPES["KHZ_8"]
|
|
320
334
|
elif param_samplerate == 16:
|
|
321
|
-
samplerate = AUDIO_CODEC_PARAM_SAMPLE_RATE_TYPES[
|
|
335
|
+
samplerate = AUDIO_CODEC_PARAM_SAMPLE_RATE_TYPES["KHZ_16"]
|
|
322
336
|
elif param_samplerate == 24:
|
|
323
|
-
samplerate = AUDIO_CODEC_PARAM_SAMPLE_RATE_TYPES[
|
|
337
|
+
samplerate = AUDIO_CODEC_PARAM_SAMPLE_RATE_TYPES["KHZ_24"]
|
|
324
338
|
else:
|
|
325
|
-
logger.warning(
|
|
339
|
+
logger.warning("Unsupported sample rate %s", param_samplerate)
|
|
326
340
|
continue
|
|
327
341
|
|
|
328
|
-
param_tlv = tlv.encode(
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
342
|
+
param_tlv = tlv.encode(
|
|
343
|
+
AUDIO_CODEC_PARAM_TYPES["CHANNEL"],
|
|
344
|
+
b"\x01",
|
|
345
|
+
AUDIO_CODEC_PARAM_TYPES["BIT_RATE"],
|
|
346
|
+
bitrate,
|
|
347
|
+
AUDIO_CODEC_PARAM_TYPES["SAMPLE_RATE"],
|
|
348
|
+
samplerate,
|
|
349
|
+
)
|
|
350
|
+
config_tlv = tlv.encode(
|
|
351
|
+
AUDIO_TYPES["CODEC"], codec, AUDIO_TYPES["CODEC_PARAM"], param_tlv
|
|
352
|
+
)
|
|
333
353
|
configs += tlv.encode(SUPPORTED_AUDIO_CODECS_TAG, config_tlv)
|
|
334
354
|
|
|
335
355
|
if not has_supported_codec:
|
|
336
|
-
logger.warning(
|
|
356
|
+
logger.warning("Client does not support any audio codec that iOS supports.")
|
|
337
357
|
|
|
338
|
-
codec = AUDIO_CODEC_TYPES[
|
|
339
|
-
bitrate = AUDIO_CODEC_PARAM_BIT_RATE_TYPES[
|
|
340
|
-
samplerate = AUDIO_CODEC_PARAM_SAMPLE_RATE_TYPES[
|
|
358
|
+
codec = AUDIO_CODEC_TYPES["OPUS"]
|
|
359
|
+
bitrate = AUDIO_CODEC_PARAM_BIT_RATE_TYPES["VARIABLE"]
|
|
360
|
+
samplerate = AUDIO_CODEC_PARAM_SAMPLE_RATE_TYPES["KHZ_24"]
|
|
341
361
|
|
|
342
362
|
param_tlv = tlv.encode(
|
|
343
|
-
AUDIO_CODEC_PARAM_TYPES[
|
|
344
|
-
|
|
345
|
-
AUDIO_CODEC_PARAM_TYPES[
|
|
363
|
+
AUDIO_CODEC_PARAM_TYPES["CHANNEL"],
|
|
364
|
+
b"\x01",
|
|
365
|
+
AUDIO_CODEC_PARAM_TYPES["BIT_RATE"],
|
|
366
|
+
bitrate,
|
|
367
|
+
AUDIO_CODEC_PARAM_TYPES["SAMPLE_RATE"],
|
|
368
|
+
samplerate,
|
|
369
|
+
)
|
|
346
370
|
|
|
347
|
-
config_tlv = tlv.encode(
|
|
348
|
-
|
|
371
|
+
config_tlv = tlv.encode(
|
|
372
|
+
AUDIO_TYPES["CODEC"], codec, AUDIO_TYPES["CODEC_PARAM"], param_tlv
|
|
373
|
+
)
|
|
349
374
|
|
|
350
375
|
configs = tlv.encode(SUPPORTED_AUDIO_CODECS_TAG, config_tlv)
|
|
351
376
|
|
|
352
|
-
comfort_noise = byte_bool(
|
|
353
|
-
audio_params.get('comfort_noise', False))
|
|
377
|
+
comfort_noise = byte_bool(audio_params.get("comfort_noise", False))
|
|
354
378
|
audio_config = to_base64_str(
|
|
355
|
-
|
|
379
|
+
configs + tlv.encode(SUPPORTED_COMFORT_NOISE_TAG, comfort_noise)
|
|
380
|
+
)
|
|
356
381
|
return audio_config
|
|
357
382
|
|
|
358
383
|
def __init__(self, options, *args, **kwargs):
|
|
@@ -409,20 +434,20 @@ class Camera(Accessory):
|
|
|
409
434
|
|
|
410
435
|
:type options: ``dict``
|
|
411
436
|
"""
|
|
412
|
-
self.has_srtp = options.get(
|
|
413
|
-
self.start_stream_cmd = options.get(
|
|
437
|
+
self.has_srtp = options.get("srtp", False)
|
|
438
|
+
self.start_stream_cmd = options.get("start_stream_cmd", FFMPEG_CMD)
|
|
414
439
|
|
|
415
|
-
self.stream_address = options[
|
|
440
|
+
self.stream_address = options["address"]
|
|
416
441
|
try:
|
|
417
442
|
ipaddress.IPv4Address(self.stream_address)
|
|
418
|
-
self.stream_address_isv6 = b
|
|
443
|
+
self.stream_address_isv6 = b"\x00"
|
|
419
444
|
except ValueError:
|
|
420
|
-
self.stream_address_isv6 = b
|
|
445
|
+
self.stream_address_isv6 = b"\x01"
|
|
421
446
|
self.sessions = {}
|
|
422
447
|
|
|
423
448
|
super().__init__(*args, **kwargs)
|
|
424
449
|
|
|
425
|
-
self.add_preload_service(
|
|
450
|
+
self.add_preload_service("Microphone")
|
|
426
451
|
self._streaming_status = []
|
|
427
452
|
self._management = []
|
|
428
453
|
self._setup_stream_management(options)
|
|
@@ -441,7 +466,9 @@ class Camera(Accessory):
|
|
|
441
466
|
|
|
442
467
|
def _create_stream_management(self, stream_idx, options):
|
|
443
468
|
"""Create a stream management service."""
|
|
444
|
-
management = self.add_preload_service(
|
|
469
|
+
management = self.add_preload_service(
|
|
470
|
+
"CameraRTPStreamManagement", unique_id=stream_idx
|
|
471
|
+
)
|
|
445
472
|
management.configure_char(
|
|
446
473
|
"StreamingStatus",
|
|
447
474
|
getter_callback=lambda: self._get_streaming_status(stream_idx),
|
|
@@ -484,105 +511,129 @@ class Camera(Accessory):
|
|
|
484
511
|
started.
|
|
485
512
|
:type reconfigure: bool
|
|
486
513
|
"""
|
|
487
|
-
video_tlv = objs.get(SELECTED_STREAM_CONFIGURATION_TYPES[
|
|
488
|
-
audio_tlv = objs.get(SELECTED_STREAM_CONFIGURATION_TYPES[
|
|
514
|
+
video_tlv = objs.get(SELECTED_STREAM_CONFIGURATION_TYPES["VIDEO"])
|
|
515
|
+
audio_tlv = objs.get(SELECTED_STREAM_CONFIGURATION_TYPES["AUDIO"])
|
|
489
516
|
|
|
490
517
|
opts = {}
|
|
491
518
|
|
|
492
519
|
if video_tlv:
|
|
493
520
|
video_objs = tlv.decode(video_tlv)
|
|
494
521
|
|
|
495
|
-
video_codec_params = video_objs.get(VIDEO_TYPES[
|
|
522
|
+
video_codec_params = video_objs.get(VIDEO_TYPES["CODEC_PARAM"])
|
|
496
523
|
if video_codec_params:
|
|
497
524
|
video_codec_param_objs = tlv.decode(video_codec_params)
|
|
498
|
-
opts[
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
525
|
+
opts["v_profile_id"] = video_codec_param_objs[
|
|
526
|
+
VIDEO_CODEC_PARAM_TYPES["PROFILE_ID"]
|
|
527
|
+
]
|
|
528
|
+
opts["v_level"] = video_codec_param_objs[
|
|
529
|
+
VIDEO_CODEC_PARAM_TYPES["LEVEL"]
|
|
530
|
+
]
|
|
531
|
+
|
|
532
|
+
video_attrs = video_objs.get(VIDEO_TYPES["ATTRIBUTES"])
|
|
504
533
|
if video_attrs:
|
|
505
534
|
video_attr_objs = tlv.decode(video_attrs)
|
|
506
|
-
opts[
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
535
|
+
opts["width"] = struct.unpack(
|
|
536
|
+
"<H", video_attr_objs[VIDEO_ATTRIBUTES_TYPES["IMAGE_WIDTH"]]
|
|
537
|
+
)[0]
|
|
538
|
+
opts["height"] = struct.unpack(
|
|
539
|
+
"<H", video_attr_objs[VIDEO_ATTRIBUTES_TYPES["IMAGE_HEIGHT"]]
|
|
540
|
+
)[0]
|
|
541
|
+
opts["fps"] = struct.unpack(
|
|
542
|
+
"<B", video_attr_objs[VIDEO_ATTRIBUTES_TYPES["FRAME_RATE"]]
|
|
543
|
+
)[0]
|
|
544
|
+
|
|
545
|
+
video_rtp_param = video_objs.get(VIDEO_TYPES["RTP_PARAM"])
|
|
514
546
|
if video_rtp_param:
|
|
515
547
|
video_rtp_param_objs = tlv.decode(video_rtp_param)
|
|
516
|
-
if RTP_PARAM_TYPES[
|
|
517
|
-
opts[
|
|
548
|
+
if RTP_PARAM_TYPES["SYNCHRONIZATION_SOURCE"] in video_rtp_param_objs:
|
|
549
|
+
opts["v_ssrc"] = struct.unpack(
|
|
550
|
+
"<I",
|
|
518
551
|
video_rtp_param_objs.get(
|
|
519
|
-
RTP_PARAM_TYPES[
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
if RTP_PARAM_TYPES[
|
|
527
|
-
opts[
|
|
528
|
-
video_rtp_param_objs.get(RTP_PARAM_TYPES[
|
|
529
|
-
|
|
530
|
-
|
|
552
|
+
RTP_PARAM_TYPES["SYNCHRONIZATION_SOURCE"]
|
|
553
|
+
),
|
|
554
|
+
)[0]
|
|
555
|
+
if RTP_PARAM_TYPES["PAYLOAD_TYPE"] in video_rtp_param_objs:
|
|
556
|
+
opts["v_payload_type"] = video_rtp_param_objs.get(
|
|
557
|
+
RTP_PARAM_TYPES["PAYLOAD_TYPE"]
|
|
558
|
+
)
|
|
559
|
+
if RTP_PARAM_TYPES["MAX_BIT_RATE"] in video_rtp_param_objs:
|
|
560
|
+
opts["v_max_bitrate"] = struct.unpack(
|
|
561
|
+
"<H", video_rtp_param_objs.get(RTP_PARAM_TYPES["MAX_BIT_RATE"])
|
|
562
|
+
)[0]
|
|
563
|
+
if RTP_PARAM_TYPES["RTCP_SEND_INTERVAL"] in video_rtp_param_objs:
|
|
564
|
+
opts["v_rtcp_interval"] = struct.unpack(
|
|
565
|
+
"<f",
|
|
566
|
+
video_rtp_param_objs.get(RTP_PARAM_TYPES["RTCP_SEND_INTERVAL"]),
|
|
567
|
+
)[0]
|
|
568
|
+
if RTP_PARAM_TYPES["MAX_MTU"] in video_rtp_param_objs:
|
|
569
|
+
opts["v_max_mtu"] = video_rtp_param_objs.get(
|
|
570
|
+
RTP_PARAM_TYPES["MAX_MTU"]
|
|
571
|
+
)
|
|
531
572
|
|
|
532
573
|
if audio_tlv:
|
|
533
574
|
audio_objs = tlv.decode(audio_tlv)
|
|
534
575
|
|
|
535
|
-
opts[
|
|
536
|
-
audio_codec_param_objs = tlv.decode(
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
opts[
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
opts[
|
|
547
|
-
1 + audio_codec_param_objs[AUDIO_CODEC_PARAM_TYPES[
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
opts[
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
opts[
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
576
|
+
opts["a_codec"] = audio_objs[AUDIO_TYPES["CODEC"]]
|
|
577
|
+
audio_codec_param_objs = tlv.decode(audio_objs[AUDIO_TYPES["CODEC_PARAM"]])
|
|
578
|
+
audio_rtp_param_objs = tlv.decode(audio_objs[AUDIO_TYPES["RTP_PARAM"]])
|
|
579
|
+
opts["a_comfort_noise"] = audio_objs[AUDIO_TYPES["COMFORT_NOISE"]]
|
|
580
|
+
|
|
581
|
+
opts["a_channel"] = audio_codec_param_objs[
|
|
582
|
+
AUDIO_CODEC_PARAM_TYPES["CHANNEL"]
|
|
583
|
+
][0]
|
|
584
|
+
opts["a_bitrate"] = struct.unpack(
|
|
585
|
+
"?", audio_codec_param_objs[AUDIO_CODEC_PARAM_TYPES["BIT_RATE"]]
|
|
586
|
+
)[0]
|
|
587
|
+
opts["a_sample_rate"] = 8 * (
|
|
588
|
+
1 + audio_codec_param_objs[AUDIO_CODEC_PARAM_TYPES["SAMPLE_RATE"]][0]
|
|
589
|
+
)
|
|
590
|
+
opts["a_packet_time"] = struct.unpack(
|
|
591
|
+
"<B", audio_codec_param_objs[AUDIO_CODEC_PARAM_TYPES["PACKET_TIME"]]
|
|
592
|
+
)[0]
|
|
593
|
+
|
|
594
|
+
opts["a_ssrc"] = struct.unpack(
|
|
595
|
+
"<I", audio_rtp_param_objs[RTP_PARAM_TYPES["SYNCHRONIZATION_SOURCE"]]
|
|
596
|
+
)[0]
|
|
597
|
+
opts["a_payload_type"] = audio_rtp_param_objs[
|
|
598
|
+
RTP_PARAM_TYPES["PAYLOAD_TYPE"]
|
|
599
|
+
]
|
|
600
|
+
opts["a_max_bitrate"] = struct.unpack(
|
|
601
|
+
"<H", audio_rtp_param_objs[RTP_PARAM_TYPES["MAX_BIT_RATE"]]
|
|
602
|
+
)[0]
|
|
603
|
+
opts["a_rtcp_interval"] = struct.unpack(
|
|
604
|
+
"<f", audio_rtp_param_objs[RTP_PARAM_TYPES["RTCP_SEND_INTERVAL"]]
|
|
605
|
+
)[0]
|
|
606
|
+
opts["a_comfort_payload_type"] = audio_rtp_param_objs[
|
|
607
|
+
RTP_PARAM_TYPES["COMFORT_NOISE_PAYLOAD_TYPE"]
|
|
608
|
+
]
|
|
609
|
+
|
|
610
|
+
session_objs = tlv.decode(objs[SELECTED_STREAM_CONFIGURATION_TYPES["SESSION"]])
|
|
611
|
+
session_id = UUID(bytes=session_objs[SETUP_TYPES["SESSION_ID"]])
|
|
563
612
|
session_info = self.sessions[session_id]
|
|
564
|
-
stream_idx = session_info[
|
|
613
|
+
stream_idx = session_info["stream_idx"]
|
|
565
614
|
|
|
566
615
|
opts.update(session_info)
|
|
567
|
-
success =
|
|
616
|
+
success = (
|
|
617
|
+
await self.reconfigure_stream(session_info, opts)
|
|
618
|
+
if reconfigure
|
|
568
619
|
else await self.start_stream(session_info, opts)
|
|
620
|
+
)
|
|
569
621
|
|
|
570
622
|
if success:
|
|
571
|
-
self._streaming_status[stream_idx] = STREAMING_STATUS[
|
|
623
|
+
self._streaming_status[stream_idx] = STREAMING_STATUS["STREAMING"]
|
|
572
624
|
else:
|
|
573
625
|
logger.error(
|
|
574
|
-
|
|
575
|
-
session_id
|
|
626
|
+
"[%s] Failed to start/reconfigure stream, deleting session.", session_id
|
|
576
627
|
)
|
|
577
628
|
del self.sessions[session_id]
|
|
578
|
-
self._streaming_status[stream_idx] = STREAMING_STATUS[
|
|
629
|
+
self._streaming_status[stream_idx] = STREAMING_STATUS["AVAILABLE"]
|
|
579
630
|
|
|
580
631
|
def _get_streaming_status(self, stream_idx):
|
|
581
632
|
"""Get the streaming status in TLV format.
|
|
582
633
|
|
|
583
634
|
Called when iOS reads the StreaminStatus ``Characteristic``.
|
|
584
635
|
"""
|
|
585
|
-
return tlv.encode(b
|
|
636
|
+
return tlv.encode(b"\x01", self._streaming_status[stream_idx], to_base64=True)
|
|
586
637
|
|
|
587
638
|
async def _stop_stream(self, objs):
|
|
588
639
|
"""Stop the stream for the specified session.
|
|
@@ -592,23 +643,23 @@ class Camera(Accessory):
|
|
|
592
643
|
:param objs: TLV-decoded SelectedRTPStreamConfiguration value.
|
|
593
644
|
:param objs: ``dict``
|
|
594
645
|
"""
|
|
595
|
-
session_objs = tlv.decode(objs[SELECTED_STREAM_CONFIGURATION_TYPES[
|
|
596
|
-
session_id = UUID(bytes=session_objs[SETUP_TYPES[
|
|
646
|
+
session_objs = tlv.decode(objs[SELECTED_STREAM_CONFIGURATION_TYPES["SESSION"]])
|
|
647
|
+
session_id = UUID(bytes=session_objs[SETUP_TYPES["SESSION_ID"]])
|
|
597
648
|
|
|
598
649
|
session_info = self.sessions.get(session_id)
|
|
599
650
|
if not session_info:
|
|
600
651
|
logger.error(
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
session_id
|
|
652
|
+
"Requested to stop stream for session %s, but no "
|
|
653
|
+
"such session was found",
|
|
654
|
+
session_id,
|
|
604
655
|
)
|
|
605
656
|
return
|
|
606
657
|
|
|
607
|
-
stream_idx = session_info[
|
|
658
|
+
stream_idx = session_info["stream_idx"]
|
|
608
659
|
await self.stop_stream(session_info)
|
|
609
660
|
del self.sessions[session_id]
|
|
610
661
|
|
|
611
|
-
self._streaming_status[stream_idx] = STREAMING_STATUS[
|
|
662
|
+
self._streaming_status[stream_idx] = STREAMING_STATUS["AVAILABLE"]
|
|
612
663
|
|
|
613
664
|
def set_selected_stream_configuration(self, value):
|
|
614
665
|
"""Set the selected stream configuration.
|
|
@@ -621,17 +672,17 @@ class Camera(Accessory):
|
|
|
621
672
|
:param value: base64-encoded selected configuration in TLV format
|
|
622
673
|
:type value: ``str``
|
|
623
674
|
"""
|
|
624
|
-
logger.debug(
|
|
675
|
+
logger.debug("set_selected_stream_config - value - %s", value)
|
|
625
676
|
|
|
626
677
|
objs = tlv.decode(value, from_base64=True)
|
|
627
|
-
if SELECTED_STREAM_CONFIGURATION_TYPES[
|
|
628
|
-
logger.error(
|
|
678
|
+
if SELECTED_STREAM_CONFIGURATION_TYPES["SESSION"] not in objs:
|
|
679
|
+
logger.error("Bad request to set selected stream configuration.")
|
|
629
680
|
return
|
|
630
681
|
|
|
631
|
-
session = tlv.decode(objs[SELECTED_STREAM_CONFIGURATION_TYPES[
|
|
682
|
+
session = tlv.decode(objs[SELECTED_STREAM_CONFIGURATION_TYPES["SESSION"]])
|
|
632
683
|
|
|
633
|
-
request_type = session[b
|
|
634
|
-
logger.debug(
|
|
684
|
+
request_type = session[b"\x02"][0]
|
|
685
|
+
logger.debug("Set stream config request: %d", request_type)
|
|
635
686
|
if request_type == 1:
|
|
636
687
|
job = functools.partial(self._start_stream, reconfigure=False)
|
|
637
688
|
elif request_type == 0:
|
|
@@ -639,7 +690,7 @@ class Camera(Accessory):
|
|
|
639
690
|
elif request_type == 4:
|
|
640
691
|
job = functools.partial(self._start_stream, reconfigure=True)
|
|
641
692
|
else:
|
|
642
|
-
logger.error(
|
|
693
|
+
logger.error("Unknown request type %d", request_type)
|
|
643
694
|
return
|
|
644
695
|
|
|
645
696
|
self.driver.add_job(job, objs)
|
|
@@ -662,58 +713,73 @@ class Camera(Accessory):
|
|
|
662
713
|
stream_idx = 0
|
|
663
714
|
|
|
664
715
|
objs = tlv.decode(value, from_base64=True)
|
|
665
|
-
session_id = UUID(bytes=objs[SETUP_TYPES[
|
|
716
|
+
session_id = UUID(bytes=objs[SETUP_TYPES["SESSION_ID"]])
|
|
666
717
|
|
|
667
718
|
# Extract address info
|
|
668
|
-
address_tlv = objs[SETUP_TYPES[
|
|
719
|
+
address_tlv = objs[SETUP_TYPES["ADDRESS"]]
|
|
669
720
|
address_info_objs = tlv.decode(address_tlv)
|
|
670
|
-
is_ipv6 = struct.unpack(
|
|
671
|
-
|
|
672
|
-
|
|
721
|
+
is_ipv6 = struct.unpack("?", address_info_objs[SETUP_ADDR_INFO["ADDRESS_VER"]])[
|
|
722
|
+
0
|
|
723
|
+
]
|
|
724
|
+
address = address_info_objs[SETUP_ADDR_INFO["ADDRESS"]].decode("utf8")
|
|
673
725
|
target_video_port = struct.unpack(
|
|
674
|
-
|
|
726
|
+
"<H", address_info_objs[SETUP_ADDR_INFO["VIDEO_RTP_PORT"]]
|
|
727
|
+
)[0]
|
|
675
728
|
target_audio_port = struct.unpack(
|
|
676
|
-
|
|
729
|
+
"<H", address_info_objs[SETUP_ADDR_INFO["AUDIO_RTP_PORT"]]
|
|
730
|
+
)[0]
|
|
677
731
|
|
|
678
732
|
# Video SRTP Params
|
|
679
|
-
video_srtp_tlv = objs[SETUP_TYPES[
|
|
733
|
+
video_srtp_tlv = objs[SETUP_TYPES["VIDEO_SRTP_PARAM"]]
|
|
680
734
|
video_info_objs = tlv.decode(video_srtp_tlv)
|
|
681
|
-
video_crypto_suite = video_info_objs[SETUP_SRTP_PARAM[
|
|
682
|
-
video_master_key = video_info_objs[SETUP_SRTP_PARAM[
|
|
683
|
-
video_master_salt = video_info_objs[SETUP_SRTP_PARAM[
|
|
735
|
+
video_crypto_suite = video_info_objs[SETUP_SRTP_PARAM["CRYPTO"]][0]
|
|
736
|
+
video_master_key = video_info_objs[SETUP_SRTP_PARAM["MASTER_KEY"]]
|
|
737
|
+
video_master_salt = video_info_objs[SETUP_SRTP_PARAM["MASTER_SALT"]]
|
|
684
738
|
|
|
685
739
|
# Audio SRTP Params
|
|
686
|
-
audio_srtp_tlv = objs[SETUP_TYPES[
|
|
740
|
+
audio_srtp_tlv = objs[SETUP_TYPES["AUDIO_SRTP_PARAM"]]
|
|
687
741
|
audio_info_objs = tlv.decode(audio_srtp_tlv)
|
|
688
|
-
audio_crypto_suite = audio_info_objs[SETUP_SRTP_PARAM[
|
|
689
|
-
audio_master_key = audio_info_objs[SETUP_SRTP_PARAM[
|
|
690
|
-
audio_master_salt = audio_info_objs[SETUP_SRTP_PARAM[
|
|
742
|
+
audio_crypto_suite = audio_info_objs[SETUP_SRTP_PARAM["CRYPTO"]][0]
|
|
743
|
+
audio_master_key = audio_info_objs[SETUP_SRTP_PARAM["MASTER_KEY"]]
|
|
744
|
+
audio_master_salt = audio_info_objs[SETUP_SRTP_PARAM["MASTER_SALT"]]
|
|
691
745
|
|
|
692
746
|
logger.debug(
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
session_id,
|
|
747
|
+
"Received endpoint configuration:"
|
|
748
|
+
"\nsession_id: %s\naddress: %s\nis_ipv6: %s"
|
|
749
|
+
"\ntarget_video_port: %s\ntarget_audio_port: %s"
|
|
750
|
+
"\nvideo_crypto_suite: %s\nvideo_srtp: %s"
|
|
751
|
+
"\naudio_crypto_suite: %s\naudio_srtp: %s",
|
|
752
|
+
session_id,
|
|
753
|
+
address,
|
|
754
|
+
is_ipv6,
|
|
755
|
+
target_video_port,
|
|
756
|
+
target_audio_port,
|
|
699
757
|
video_crypto_suite,
|
|
700
758
|
to_base64_str(video_master_key + video_master_salt),
|
|
701
759
|
audio_crypto_suite,
|
|
702
|
-
to_base64_str(audio_master_key + audio_master_salt)
|
|
760
|
+
to_base64_str(audio_master_key + audio_master_salt),
|
|
703
761
|
)
|
|
704
762
|
|
|
705
763
|
# Configure the SetupEndpoints response
|
|
706
764
|
|
|
707
765
|
if self.has_srtp:
|
|
708
766
|
video_srtp_tlv = tlv.encode(
|
|
709
|
-
SETUP_SRTP_PARAM[
|
|
710
|
-
|
|
711
|
-
SETUP_SRTP_PARAM[
|
|
767
|
+
SETUP_SRTP_PARAM["CRYPTO"],
|
|
768
|
+
SRTP_CRYPTO_SUITES["AES_CM_128_HMAC_SHA1_80"],
|
|
769
|
+
SETUP_SRTP_PARAM["MASTER_KEY"],
|
|
770
|
+
video_master_key,
|
|
771
|
+
SETUP_SRTP_PARAM["MASTER_SALT"],
|
|
772
|
+
video_master_salt,
|
|
773
|
+
)
|
|
712
774
|
|
|
713
775
|
audio_srtp_tlv = tlv.encode(
|
|
714
|
-
SETUP_SRTP_PARAM[
|
|
715
|
-
|
|
716
|
-
SETUP_SRTP_PARAM[
|
|
776
|
+
SETUP_SRTP_PARAM["CRYPTO"],
|
|
777
|
+
SRTP_CRYPTO_SUITES["AES_CM_128_HMAC_SHA1_80"],
|
|
778
|
+
SETUP_SRTP_PARAM["MASTER_KEY"],
|
|
779
|
+
audio_master_key,
|
|
780
|
+
SETUP_SRTP_PARAM["MASTER_SALT"],
|
|
781
|
+
audio_master_salt,
|
|
782
|
+
)
|
|
717
783
|
else:
|
|
718
784
|
video_srtp_tlv = NO_SRTP
|
|
719
785
|
audio_srtp_tlv = NO_SRTP
|
|
@@ -722,39 +788,55 @@ class Camera(Accessory):
|
|
|
722
788
|
audio_ssrc = int.from_bytes(os.urandom(3), byteorder="big")
|
|
723
789
|
|
|
724
790
|
res_address_tlv = tlv.encode(
|
|
725
|
-
SETUP_ADDR_INFO[
|
|
726
|
-
|
|
727
|
-
SETUP_ADDR_INFO[
|
|
728
|
-
|
|
791
|
+
SETUP_ADDR_INFO["ADDRESS_VER"],
|
|
792
|
+
self.stream_address_isv6,
|
|
793
|
+
SETUP_ADDR_INFO["ADDRESS"],
|
|
794
|
+
self.stream_address.encode("utf-8"),
|
|
795
|
+
SETUP_ADDR_INFO["VIDEO_RTP_PORT"],
|
|
796
|
+
struct.pack("<H", target_video_port),
|
|
797
|
+
SETUP_ADDR_INFO["AUDIO_RTP_PORT"],
|
|
798
|
+
struct.pack("<H", target_audio_port),
|
|
799
|
+
)
|
|
729
800
|
|
|
730
801
|
response_tlv = tlv.encode(
|
|
731
|
-
SETUP_TYPES[
|
|
732
|
-
|
|
733
|
-
SETUP_TYPES[
|
|
734
|
-
|
|
735
|
-
SETUP_TYPES[
|
|
736
|
-
|
|
737
|
-
SETUP_TYPES[
|
|
738
|
-
|
|
802
|
+
SETUP_TYPES["SESSION_ID"],
|
|
803
|
+
session_id.bytes,
|
|
804
|
+
SETUP_TYPES["STATUS"],
|
|
805
|
+
SETUP_STATUS["SUCCESS"],
|
|
806
|
+
SETUP_TYPES["ADDRESS"],
|
|
807
|
+
res_address_tlv,
|
|
808
|
+
SETUP_TYPES["VIDEO_SRTP_PARAM"],
|
|
809
|
+
video_srtp_tlv,
|
|
810
|
+
SETUP_TYPES["AUDIO_SRTP_PARAM"],
|
|
811
|
+
audio_srtp_tlv,
|
|
812
|
+
SETUP_TYPES["VIDEO_SSRC"],
|
|
813
|
+
struct.pack("<I", video_ssrc),
|
|
814
|
+
SETUP_TYPES["AUDIO_SSRC"],
|
|
815
|
+
struct.pack("<I", audio_ssrc),
|
|
816
|
+
to_base64=True,
|
|
817
|
+
)
|
|
739
818
|
|
|
740
819
|
self.sessions[session_id] = {
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
820
|
+
"id": session_id,
|
|
821
|
+
"stream_idx": stream_idx,
|
|
822
|
+
"address": address,
|
|
823
|
+
"v_port": target_video_port,
|
|
824
|
+
"v_srtp_key": to_base64_str(video_master_key + video_master_salt),
|
|
825
|
+
"v_ssrc": video_ssrc,
|
|
826
|
+
"a_port": target_audio_port,
|
|
827
|
+
"a_srtp_key": to_base64_str(audio_master_key + audio_master_salt),
|
|
828
|
+
"a_ssrc": audio_ssrc,
|
|
750
829
|
}
|
|
751
830
|
|
|
752
|
-
self._management[stream_idx].get_characteristic(
|
|
831
|
+
self._management[stream_idx].get_characteristic("SetupEndpoints").set_value(
|
|
832
|
+
response_tlv
|
|
833
|
+
)
|
|
753
834
|
|
|
754
835
|
async def stop(self):
|
|
755
836
|
"""Stop all streaming sessions."""
|
|
756
|
-
await asyncio.gather(
|
|
757
|
-
self.stop_stream(session_info) for session_info in self.sessions.values())
|
|
837
|
+
await asyncio.gather(
|
|
838
|
+
*(self.stop_stream(session_info) for session_info in self.sessions.values())
|
|
839
|
+
)
|
|
758
840
|
|
|
759
841
|
# ### For client extensions ###
|
|
760
842
|
|
|
@@ -816,28 +898,28 @@ class Camera(Accessory):
|
|
|
816
898
|
:rtype: ``bool``
|
|
817
899
|
"""
|
|
818
900
|
logger.debug(
|
|
819
|
-
|
|
820
|
-
session_info[
|
|
821
|
-
stream_config
|
|
901
|
+
"[%s] Starting stream with the following parameters: %s",
|
|
902
|
+
session_info["id"],
|
|
903
|
+
stream_config,
|
|
822
904
|
)
|
|
823
905
|
|
|
824
906
|
cmd = self.start_stream_cmd.format(**stream_config).split()
|
|
825
|
-
logger.debug('Executing start stream command: "%s"',
|
|
907
|
+
logger.debug('Executing start stream command: "%s"', " ".join(cmd))
|
|
826
908
|
try:
|
|
827
|
-
process = await asyncio.create_subprocess_exec(
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
909
|
+
process = await asyncio.create_subprocess_exec(
|
|
910
|
+
*cmd,
|
|
911
|
+
stdout=asyncio.subprocess.DEVNULL,
|
|
912
|
+
stderr=asyncio.subprocess.PIPE,
|
|
913
|
+
limit=1024,
|
|
914
|
+
)
|
|
831
915
|
except Exception as e: # pylint: disable=broad-except
|
|
832
|
-
logger.error(
|
|
916
|
+
logger.error("Failed to start streaming process because of error: %s", e)
|
|
833
917
|
return False
|
|
834
918
|
|
|
835
|
-
session_info[
|
|
919
|
+
session_info["process"] = process
|
|
836
920
|
|
|
837
921
|
logger.info(
|
|
838
|
-
|
|
839
|
-
session_info['id'],
|
|
840
|
-
process.pid
|
|
922
|
+
"[%s] Started stream process - PID %d", session_info["id"], process.pid
|
|
841
923
|
)
|
|
842
924
|
|
|
843
925
|
return True
|
|
@@ -853,25 +935,25 @@ class Camera(Accessory):
|
|
|
853
935
|
- id - The session ID.
|
|
854
936
|
:type session_info: ``dict``
|
|
855
937
|
"""
|
|
856
|
-
session_id = session_info[
|
|
857
|
-
ffmpeg_process = session_info.get(
|
|
938
|
+
session_id = session_info["id"]
|
|
939
|
+
ffmpeg_process = session_info.get("process")
|
|
858
940
|
if ffmpeg_process:
|
|
859
|
-
logger.info(
|
|
941
|
+
logger.info("[%s] Stopping stream.", session_id)
|
|
860
942
|
try:
|
|
861
943
|
ffmpeg_process.terminate()
|
|
862
|
-
async with async_timeout
|
|
944
|
+
async with async_timeout(2.0):
|
|
863
945
|
_, stderr = await ffmpeg_process.communicate()
|
|
864
|
-
logger.debug(
|
|
946
|
+
logger.debug("Stream command stderr: %s", stderr)
|
|
865
947
|
except asyncio.TimeoutError:
|
|
866
948
|
logger.error(
|
|
867
|
-
|
|
868
|
-
|
|
949
|
+
"Timeout while waiting for the stream process "
|
|
950
|
+
"to terminate. Trying with kill."
|
|
869
951
|
)
|
|
870
952
|
ffmpeg_process.kill()
|
|
871
953
|
await ffmpeg_process.wait()
|
|
872
|
-
logger.debug(
|
|
954
|
+
logger.debug("Stream process stopped.")
|
|
873
955
|
else:
|
|
874
|
-
logger.warning(
|
|
956
|
+
logger.warning("No process for session ID %s", session_id)
|
|
875
957
|
|
|
876
958
|
async def reconfigure_stream(self, session_info, stream_config):
|
|
877
959
|
"""Reconfigure the stream so that it uses the given ``stream_config``.
|
|
@@ -894,5 +976,5 @@ class Camera(Accessory):
|
|
|
894
976
|
:param image_size: ``dict`` describing the requested image size. Contains the
|
|
895
977
|
keys "image-width" and "image-height"
|
|
896
978
|
"""
|
|
897
|
-
with open(os.path.join(RESOURCE_DIR,
|
|
979
|
+
with open(os.path.join(RESOURCE_DIR, "snapshot.jpg"), "rb") as fp:
|
|
898
980
|
return fp.read()
|