@flashphoner/websdk 2.0.231 → 2.0.233

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.
@@ -1,4 +1,4 @@
1
- Web SDK - 2.0.231
1
+ Web SDK - 2.0.233
2
2
 
3
3
  [Download builds](https://docs.flashphoner.com/display/WEBSDK2EN/Web+SDK+release+notes)
4
4
 
@@ -2,6 +2,32 @@
2
2
  ///////////// Utils ////////////
3
3
  ///////////////////////////////////
4
4
 
5
+ /**
6
+ * Default server ports description
7
+ *
8
+ * @type {{legacy: {http: number, https: number}, wss: number, http: number, https: number, ws: number, hls: {http: number, https: number}}}
9
+ */
10
+ const DEFAULT_PORTS = {
11
+ http: 8081,
12
+ https: 8444,
13
+ legacy: {
14
+ http: 9091,
15
+ https: 8888
16
+ },
17
+ ws: 8080,
18
+ wss: 8443,
19
+ hls: {
20
+ http: 8082,
21
+ https: 8445
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Check if object is not empty
27
+ *
28
+ * @param obj object to check
29
+ * @returns {boolean} true if object is not empty
30
+ */
5
31
  function notEmpty(obj) {
6
32
  if (obj != null && obj != 'undefined' && obj != '') {
7
33
  return true;
@@ -9,34 +35,165 @@ function notEmpty(obj) {
9
35
  return false;
10
36
  }
11
37
 
12
- //Trace
38
+ /**
39
+ * Print trace string to
40
+ *
41
+ * @param str string to print
42
+ */
13
43
  function trace(str) {
14
44
  console.log(str);
15
45
  }
16
46
 
17
- //Get field
47
+ /**
48
+ * Get HTML page field
49
+ *
50
+ * @param name field name
51
+ * @returns {*} field
52
+ */
18
53
  function field(name) {
19
54
  var field = document.getElementById(name).value;
20
55
  return field;
21
56
  }
22
57
 
23
- //Set WCS URL
24
- function setURL() {
25
- var proto;
26
- var url;
27
- var port;
58
+ /**
59
+ * Set default value to port if it is not defined
60
+ *
61
+ * @param port
62
+ * @param value
63
+ * @returns {*}
64
+ */
65
+ function setDefaultPort(port, value) {
66
+ if (port === undefined) {
67
+ return value;
68
+ }
69
+ return port;
70
+ }
71
+
72
+ /**
73
+ * Get server ports configuration
74
+ *
75
+ * @returns {{legacy: {http: number, https: number}, wss: number, http: number, https: number, ws: number, hls: {http: number, https: number}}}
76
+ */
77
+ function getPortsConfig() {
78
+ let portsConfig = DEFAULT_PORTS;
79
+ try {
80
+ // Try to get ports from a separate config declaring the SERVER_PORTS constant
81
+ portsConfig = SERVER_PORTS;
82
+ // Fill the ports absent in config by default values
83
+ portsConfig.http = setDefaultPort(portsConfig.http, DEFAULT_PORTS.http);
84
+ portsConfig.https = setDefaultPort(portsConfig.https, DEFAULT_PORTS.https);
85
+ portsConfig.legacy = setDefaultPort(portsConfig.legacy, DEFAULT_PORTS.legacy);
86
+ portsConfig.legacy.http = setDefaultPort(portsConfig.legacy.http, DEFAULT_PORTS.legacy.http);
87
+ portsConfig.legacy.https = setDefaultPort(portsConfig.legacy.https, DEFAULT_PORTS.legacy.https);
88
+ portsConfig.ws = setDefaultPort(portsConfig.ws, DEFAULT_PORTS.ws);
89
+ portsConfig.wss = setDefaultPort(portsConfig.wss, DEFAULT_PORTS.wss);
90
+ portsConfig.hls = setDefaultPort(portsConfig.hls, DEFAULT_PORTS.hls);
91
+ portsConfig.hls.http = setDefaultPort(portsConfig.hls.http, DEFAULT_PORTS.hls.http);
92
+ portsConfig.hls.https = setDefaultPort(portsConfig.hls.https, DEFAULT_PORTS.hls.https);
93
+ console.log("Use custom server ports");
94
+ } catch(e) {
95
+ console.log("Use default server ports");
96
+ console.log(e.stack);
97
+ }
98
+ return portsConfig;
99
+ }
100
+
101
+ /**
102
+ * Get web interface standard port depending on connection type
103
+ *
104
+ * @returns {string}
105
+ */
106
+ function getWebPort() {
107
+ let portsConfig = getPortsConfig();
108
+ let port;
109
+ if (window.location.protocol == "http:") {
110
+ port = portsConfig.http;
111
+ } else {
112
+ port = portsConfig.https;
113
+ }
114
+ return port;
115
+ }
116
+
117
+ /**
118
+ * Get HTML protocol used depending on connection type
119
+ *
120
+ * @returns {string}
121
+ */
122
+ function getWebProtocol() {
123
+ let proto;
124
+ if (window.location.protocol == "http:") {
125
+ proto = "http://";
126
+ } else {
127
+ proto = "https://";
128
+ }
129
+ return proto;
130
+ }
131
+
132
+ /**
133
+ * Get websocket standard port depending on connection type
134
+ *
135
+ * @returns {string}
136
+ */
137
+ function getWsPort() {
138
+ let portsConfig = getPortsConfig();
139
+ let port;
140
+ if (window.location.protocol == "http:") {
141
+ port = portsConfig.ws;
142
+ } else {
143
+ port = portsConfig.wss;
144
+ }
145
+ return port;
146
+ }
147
+
148
+ /**
149
+ * Get websocket protocol type depending on connection type
150
+ *
151
+ * @returns {string}
152
+ */
153
+ function getWsProtocol() {
154
+ let proto;
28
155
  if (window.location.protocol == "http:") {
29
156
  proto = "ws://";
30
- port = "8080";
31
157
  } else {
32
158
  proto = "wss://";
33
- port = "8443";
34
159
  }
160
+ return proto;
161
+ }
162
+
163
+ /**
164
+ * Get standard HLS port depending on connection type
165
+ *
166
+ * @returns {string}
167
+ */
168
+ function getHlsPort() {
169
+ let portsConfig = getPortsConfig();
170
+ let port;
171
+ if (window.location.protocol == "http:") {
172
+ port = portsConfig.hls.http;
173
+ } else {
174
+ port = portsConfig.hls.https;
175
+ }
176
+ return port;
177
+ }
35
178
 
36
- url = proto + window.location.hostname + ":" + port;
179
+ /**
180
+ * Set default WCS websocket URL (used in most examples)
181
+ *
182
+ * @returns {string}
183
+ */
184
+ function setURL() {
185
+ var proto = getWsProtocol();
186
+ var port = getWsPort();
187
+ var url = proto + window.location.hostname + ":" + port;
37
188
  return url;
38
189
  }
39
190
 
191
+ /**
192
+ * Get URL parameter
193
+ *
194
+ * @param name
195
+ * @returns {string|null}
196
+ */
40
197
  function getUrlParam(name) {
41
198
  var url = window.location.href;
42
199
  name = name.replace(/[\[\]]/g, "\\$&");
@@ -47,40 +204,83 @@ function getUrlParam(name) {
47
204
  return decodeURIComponent(results[2].replace(/\+/g, " "));
48
205
  }
49
206
 
207
+ /**
208
+ * Get default HLS url
209
+ *
210
+ * @returns {string}
211
+ */
50
212
  function getHLSUrl() {
213
+ var proto = getWebProtocol();
214
+ var port = getHlsPort();
215
+ var url = proto + window.location.hostname + ":" + port;
216
+ return url;
217
+ }
51
218
 
52
- var proto;
219
+ /**
220
+ * Get default admin URL (used in Embed Player)
221
+ *
222
+ * @returns {string}
223
+ */
224
+ function getAdminUrl() {
225
+ var portsConfig = getPortsConfig();
226
+ var proto = getWebProtocol();
53
227
  var port;
54
-
55
228
  if (window.location.protocol == "http:") {
56
- proto = "http://";
57
- port = "8082";
229
+ port = portsConfig.legacy.http;
58
230
  } else {
59
- proto = "https://";
60
- port = "8445";
231
+ port = portsConfig.legacy.https;
61
232
  }
62
-
63
233
  var url = proto + window.location.hostname + ":" + port;
64
234
  return url;
65
235
  }
66
236
 
67
- function getAdminUrl() {
237
+ /**
238
+ * Get REST API URL
239
+ *
240
+ * @param hostName host name to override the default one
241
+ * @param hostPort host port to override the default one
242
+ * @returns {string}
243
+ */
244
+ function getRestUrl(hostName, hostPort) {
245
+ let proto = getWebProtocol();
246
+ let port = getWebPort();
247
+ if (!hostName) {
248
+ hostName = window.location.hostname;
249
+ }
250
+ if (hostPort) {
251
+ port = hostPort;
252
+ }
68
253
 
69
- var proto;
70
- var port;
71
- if (window.location.protocol == "http:") {
72
- proto = "http://";
73
- port = "9091";
74
- } else {
75
- proto = "https://";
76
- port = "8888";
254
+ let url = proto + hostName + ":" + port;
255
+ return url;
256
+ }
257
+
258
+ /**
259
+ * Get Websocket URL
260
+ *
261
+ * @param hostName host name to override the default one
262
+ * @param hostPort host port to override the default one
263
+ * @returns {string}
264
+ */
265
+ function getWebsocketUrl(hostName, hostPort) {
266
+ let proto = getWsProtocol();
267
+ let port = getWsPort();
268
+ if (!hostName) {
269
+ hostName = window.location.hostname;
270
+ }
271
+ if (hostPort) {
272
+ port = hostPort;
77
273
  }
78
274
 
79
- var url = proto + window.location.hostname + ":" + port;
275
+ let url = proto + hostName + ":" + port;
80
276
  return url;
81
277
  }
82
278
 
83
- // Detect IE
279
+ /**
280
+ * Detect IE (for compatibility only)
281
+ *
282
+ * @returns {boolean}
283
+ */
84
284
  function detectIE() {
85
285
  var ua = window.navigator.userAgent;
86
286
  var msie = ua.indexOf('MSIE ');
@@ -94,7 +294,10 @@ function detectIE() {
94
294
  return false;
95
295
  }
96
296
 
97
- // Detect Flash
297
+ /**
298
+ * Detect Flash (for compatibility only)
299
+ *
300
+ */
98
301
  function detectFlash() {
99
302
  var hasFlash = false;
100
303
  try {
@@ -133,7 +336,12 @@ $(function () {
133
336
  });
134
337
  });
135
338
 
136
- // Generate simple uuid
339
+ /**
340
+ * Generate simple uuid
341
+ *
342
+ * @param length UUID length
343
+ * @returns {string}
344
+ */
137
345
  function createUUID(length) {
138
346
  var s = [];
139
347
  var hexDigits = "0123456789abcdef";
@@ -185,7 +393,15 @@ function resizeVideo(video, width, height) {
185
393
  console.log("Resize from " + video.videoWidth + "x" + video.videoHeight + " to " + display.offsetWidth + "x" + display.offsetHeight);
186
394
  }
187
395
 
188
-
396
+ /**
397
+ * Helper function to resize video tag
398
+ *
399
+ * @param videoWidth
400
+ * @param videoHeight
401
+ * @param dstWidth
402
+ * @param dstHeight
403
+ * @returns {{w: number, h: number}}
404
+ */
189
405
  function downScaleToFitSize(videoWidth, videoHeight, dstWidth, dstHeight) {
190
406
  var newWidth, newHeight;
191
407
  var videoRatio = videoWidth / videoHeight;
@@ -203,6 +419,11 @@ function downScaleToFitSize(videoWidth, videoHeight, dstWidth, dstHeight) {
203
419
  };
204
420
  }
205
421
 
422
+ /**
423
+ * Set Webkit fullscreen handler functions to video tag
424
+ *
425
+ * @param video
426
+ */
206
427
  function setWebkitFullscreenHandlers(video) {
207
428
  if (video) {
208
429
  let needRestart = false;
@@ -239,6 +460,12 @@ function setWebkitFullscreenHandlers(video) {
239
460
  }
240
461
  }
241
462
 
463
+ /**
464
+ * Check if fullscreen mode is available in Webkit
465
+ *
466
+ * @param video
467
+ * @returns {boolean}
468
+ */
242
469
  function canWebkitFullScreen(video) {
243
470
  let canFullscreen = false;
244
471
  if (video) {
@@ -8,6 +8,7 @@
8
8
  <link rel="stylesheet" href="console.css">
9
9
  <link rel="stylesheet" type="text/css" href="jquery.dataTables.css">
10
10
  <title>Console</title>
11
+ <script type="text/javascript" src="ports.js"></script>
11
12
  <script type="text/javascript" src="../../dependencies/jquery/jquery-1.12.0.js"></script>
12
13
  <script type="text/javascript" src="../../dependencies/jquery/jquery-ui.js"></script>
13
14
  <script type="text/javascript" src="../../dependencies/bootstrap/js/bootstrap.min.js"></script>
@@ -52,6 +53,7 @@
52
53
  <button id="pullRtspStreamModalBtn" data-toggle='modal' data-target='#pullRtspModal' type="button" class="btn btn-default btn-success btn-block">Pull RTSP stream</button>
53
54
  <hr>
54
55
  <button id="pullStreamBatchModalBtn" data-toggle='modal' data-target='#pullStreamBatchModal' type="button" class="btn btn-default btn-success btn-block">Pull streams</button>
56
+ <button id="pushStreamBatchModalBtn" data-toggle='modal' data-target='#pushStreamBatchModal' type="button" class="btn btn-default btn-success btn-block">Push streams</button>
55
57
  <button id="registerBatchModalBtn" data-toggle='modal' data-target='#registerBatchModal' type="button" class="btn btn-default btn-success btn-block">Register</button>
56
58
  <button id="unregisterBatchModalBtn" data-toggle='modal' data-target='#unregisterBatchModal' type="button" class="btn btn-default btn-success btn-block">Unregister</button>
57
59
  <button id="callBatchModalBtn" data-toggle='modal' data-target='#callBatchModal' type="button" class="btn btn-default btn-success btn-block">Call</button>
@@ -199,7 +201,7 @@
199
201
  </select>
200
202
  </div>
201
203
  <div class="form-group">
202
- <label for="pullStreamBatchNodes"><span class="glyphicon glyphicon-play-circle"></span> Proto pull</label>
204
+ <label for="pullStreamBatchProto"><span class="glyphicon glyphicon-play-circle"></span> Proto pull</label>
203
205
  <select class="custom-select" id="pullStreamBatchProto">
204
206
  <option value="ws" selected>WebRTC</option>
205
207
  <option value="rtmp">RTMP</option>
@@ -226,6 +228,48 @@
226
228
  </div>
227
229
  </div>
228
230
  </div>
231
+ <div id="pushStreamBatchModal" class="modal fade" role="dialog">
232
+ <div class="modal-dialog">
233
+ <div class="modal-content">
234
+ <div class="modal-header">
235
+ <button type="button" class="close" data-dismiss="modal">&times;</button>
236
+ <h4>Push Streams</h4>
237
+ </div>
238
+ <div class="modal-body">
239
+ <form action="" id="pushStreamBatchForm">
240
+ <div class="form-group">
241
+ <label for="pushStreamBatchNodes"><span class="glyphicon glyphicon-globe"></span> Choose node</label>
242
+ <select class="custom-select" id="pushStreamBatchNodes">
243
+ </select>
244
+ </div>
245
+ <div class="form-group">
246
+ <label for="pushStreamBatchProto"><span class="glyphicon glyphicon-play-circle"></span> Proto push</label>
247
+ <select class="custom-select" id="pushStreamBatchProto">
248
+ <option value="ws" selected>WebRTC</option>
249
+ <option value="rtmp">RTMP</option>
250
+ </select>
251
+ </div>
252
+ <div class="form-group">
253
+ <label for="pushBatchLocalName"><span class="glyphicon glyphicon-play-circle"></span> Local stream name</label>
254
+ <input type="text" class="form-control" id="pushBatchLocalName" placeholder="my_local_stream">
255
+ </div>
256
+ <div class="form-group">
257
+ <label for="pushBatchRemoteName"><span class="glyphicon glyphicon-play-circle"></span> Remote stream name</label>
258
+ <input type="text" class="form-control" id="pushBatchRemoteName" placeholder="my_remote_stream">
259
+ </div>
260
+ <div class="form-group">
261
+ <label for="pushBatchQty"><span class="glyphicon glyphicon-plus"></span> Qty</label>
262
+ <input type="text" class="form-control" id="pushBatchQty" placeholder="1">
263
+ </div>
264
+ <button id="pushBatchStream" type="button" class="btn btn-default btn-success btn-block"><span class="glyphicon glyphicon-off"></span> Push</button>
265
+ </form>
266
+ </div>
267
+ <div class="modal-footer">
268
+ <button type="button" class="btn btn-default pull-left" data-dismiss="modal"><span class="glyphicon glyphicon-remove"></span> Cancel</button>
269
+ </div>
270
+ </div>
271
+ </div>
272
+ </div>
229
273
  <div id="registerBatchModal" class="modal fade" role="dialog">
230
274
  <div class="modal-dialog">
231
275
  <div class="modal-content">
@@ -67,9 +67,15 @@ $(function() {
67
67
  $('#pullStreamBatchModal').on('show.bs.modal', function(e) {
68
68
  populateNodes($("#pullStreamBatchNodes"));
69
69
  });
70
+ $('#pushStreamBatchModal').on('show.bs.modal', function(e) {
71
+ populateNodes($("#pushStreamBatchNodes"));
72
+ });
70
73
  $("#pullBatchStream").on("click", function(e){
71
74
  pullStreamBatch();
72
75
  });
76
+ $("#pushBatchStream").on("click", function(e){
77
+ pushStreamBatch();
78
+ });
73
79
  $('#registerBatchModal').on('show.bs.modal', function(e) {
74
80
  populateNodes($("#registerBatchNodes"));
75
81
  });
@@ -146,18 +152,19 @@ $(function() {
146
152
 
147
153
  function createNode(id, ip) {
148
154
  let nodeIp = ip;
149
- let port = 8081;
155
+ let port = getWebPort();
150
156
  if (ip.indexOf(':') !== -1) {
151
157
  port = ip.substring(ip.indexOf(":") + 1);
152
158
  nodeIp = ip.substring(0, ip.indexOf(":"));
153
159
  }
154
- var api = FlashphonerRestApi.instance("http://"+nodeIp+":"+port, "http://"+nodeIp+":"+port);
160
+ let restUrl = getRestUrl(nodeIp, port);
161
+ let api = FlashphonerRestApi.instance(restUrl, restUrl);
155
162
  api.id = id;
156
163
  api.ip = nodeIp;
157
164
  api.port = port;
158
165
  api.tests = [];
159
- var state = NODE_STATE.NEW;
160
- var pollState = function(){
166
+ let state = NODE_STATE.NEW;
167
+ let pollState = function(){
161
168
  api.stat.poll().then(function(stat){
162
169
  api.setState(NODE_STATE.ALIVE, stat);
163
170
  setTimeout(pollState, REFRESH_NODE_STATE_INTERVAL);
@@ -269,7 +276,7 @@ function createNode(id, ip) {
269
276
  }
270
277
  };
271
278
  api.getTranscodingGroupStat = async function() {
272
- const result = await fetch("http://" + nodeIp + ":" + port + "/?action=stat&format=json&groups=transcoding_stats")
279
+ const result = await fetch(restUrl + "/?action=stat&format=json&groups=transcoding_stats")
273
280
  const json = await result.json();
274
281
  return json;
275
282
  }
@@ -277,10 +284,7 @@ function createNode(id, ip) {
277
284
  }
278
285
 
279
286
  function addNode(ip) {
280
- var id = ip.replace(/\./g, "");
281
- if (ip.indexOf(':') !== -1) {
282
- id = ip.substring(ip.indexOf(":") + 1).replace(/\./g, "");
283
- }
287
+ var id = ip.replace(/[.:]/g, "");
284
288
  if (nodes[id]) {
285
289
  return;
286
290
  }
@@ -471,7 +475,7 @@ function pullStreamBatch() {
471
475
  var remoteName = $("#pullBatchRemoteName").val();
472
476
  var remote;
473
477
  if (proto == "ws") {
474
- remote = "ws://" + $("#pullStreamBatchNodes").val() + ":8080/";
478
+ remote = getWebsocketUrl($("#pullStreamBatchNodes").val()) + "/";
475
479
  } else {
476
480
  remote = "rtmp://" + $("#pullStreamBatchNodes").val() + ":1935/live/" + remoteName;
477
481
  }
@@ -487,6 +491,37 @@ function pullStreamBatch() {
487
491
  $("#pullStreamBatchModal").modal('hide');
488
492
  }
489
493
 
494
+ function pushStreamBatch() {
495
+ if (!$("#pushStreamBatchNodes").val()) {
496
+ $('#warningModal').modal();
497
+ return false;
498
+ }
499
+ var node = getActiveNode();
500
+ var proto = $("#pushStreamBatchProto").val();
501
+ var localName = $("#pushBatchLocalName").val();
502
+ var remoteName = $("#pushBatchRemoteName").val();
503
+ var remote;
504
+ if (proto == "ws") {
505
+ remote = getWebsocketUrl($("#pushStreamBatchNodes").val()) + "/";
506
+ } else {
507
+ remote = "rtmp://" + $("#pushStreamBatchNodes").val() + ":1935/live/";
508
+ }
509
+ var qty = parseInt($("#pushBatchQty").val());
510
+ var interval = setInterval(function(){
511
+ if (qty > 0) {
512
+ if (proto == "ws") {
513
+ node.pull.push(remote, localName, remoteName + qty).catch(function (e) {console.log(e)});
514
+ } else {
515
+ node.push.push(localName, remote + remoteName + qty, true);
516
+ }
517
+ qty--;
518
+ } else {
519
+ clearInterval(interval);
520
+ }
521
+ }, 200);
522
+ $("#pushStreamBatchModal").modal('hide');
523
+ }
524
+
490
525
  function registerBatch() {
491
526
  if (!$("#registerBatchNodes").val()) {
492
527
  $('#warningModal').modal();
@@ -674,7 +709,7 @@ function doRego(node, testedNode, login, domain, password) {
674
709
  sipPassword: password,
675
710
  sipPort: "5060",
676
711
  sipRegisterRequired: true,
677
- urlServer: "ws://"+testedNode+":8080/"
712
+ urlServer: getWebsocketUrl(testedNode) + "/"
678
713
  };
679
714
  return node.api.createSession(connection);
680
715
  }
@@ -954,7 +989,7 @@ function streamPlayStressTest() {
954
989
  return;
955
990
  }
956
991
 
957
- var remote = "ws://" + $("#streamStressBatchNodes").val() + ":8080";
992
+ var remote = getWebsocketUrl($("#streamStressBatchNodes").val());
958
993
  var name = $("#streamStressBatchName").val();
959
994
  var start = parseInt($("#streamStressBatchStart").val());
960
995
  var end = parseInt($("#streamStressBatchEnd").val());
@@ -1323,7 +1358,7 @@ function streamPublishStressTest() {
1323
1358
  return false;
1324
1359
  }
1325
1360
  var node = getActiveNode();
1326
- var remote = "ws://" + $("#streamPublishStressBatchNodes").val() + ":8080";
1361
+ var remote = getWebsocketUrl($("#streamPublishStressBatchNodes").val());
1327
1362
  var name = $("#streamPublishStressBatchName").val();
1328
1363
  var start = parseInt($("#streamPublishStressBatchStart").val());
1329
1364
  var end = parseInt($("#streamPublishStressBatchEnd").val());
@@ -1443,7 +1478,7 @@ function streamPlayStressTestRandom() {
1443
1478
  var node = getActiveNode();
1444
1479
  var streams = {};
1445
1480
  var remoteNode = getNode($("#streamStressBatchNodes").val());
1446
- var remote = "ws://" + $("#streamStressBatchNodes").val() + ":8080";
1481
+ var remote = getWebsocketUrl($("#streamStressBatchNodes").val());
1447
1482
  var start = 0;
1448
1483
  var end = parseInt($("#streamStressMaxStreams").val());
1449
1484
  var rate = parseInt($("#streamStressBatchRate").val());
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Server ports configuration for load testing
3
+ *
4
+ */
5
+ const SERVER_PORTS = {
6
+ http: 8081,
7
+ https: 8444,
8
+ legacy: {
9
+ http: 9091,
10
+ https: 8888
11
+ },
12
+ ws: 8080,
13
+ wss: 8443,
14
+ hls: {
15
+ http: 8082,
16
+ https: 8445
17
+ }
18
+ }