android-emulator-webrtc 1.0.18 → 2.0.1
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.
- package/README.md +93 -204
- package/dist/components/emulator/emulator.d.ts +64 -0
- package/dist/components/emulator/emulator.js +161 -173
- package/dist/components/emulator/net/emulator_status.d.ts +45 -0
- package/dist/components/emulator/net/emulator_status.js +35 -42
- package/dist/components/emulator/net/logcat.js +37 -54
- package/dist/components/emulator/net/logger.d.ts +7 -0
- package/dist/components/emulator/net/logger.js +20 -0
- package/dist/components/emulator/net/ws_jsep_protocol_driver.d.ts +213 -0
- package/dist/components/emulator/net/ws_jsep_protocol_driver.js +640 -0
- package/dist/components/emulator/views/event_handler.d.ts +37 -0
- package/dist/components/emulator/views/event_handler.js +274 -260
- package/dist/components/emulator/views/webrtc_view.d.ts +27 -0
- package/dist/components/emulator/views/webrtc_view.js +42 -20
- package/dist/index.d.ts +8 -0
- package/dist/index.js +0 -7
- package/dist/proto/emulator_controller_pb.d.ts +1 -0
- package/dist/proto/emulator_controller_pb.js +146 -287
- package/dist/src/components/emulator/emulator.d.ts +64 -0
- package/dist/src/components/emulator/net/emulator_status.d.ts +45 -0
- package/dist/src/components/emulator/net/logger.d.ts +7 -0
- package/dist/src/components/emulator/net/ws_jsep_protocol_driver.d.ts +213 -0
- package/dist/src/components/emulator/views/event_handler.d.ts +37 -0
- package/dist/src/components/emulator/views/webrtc_view.d.ts +27 -0
- package/dist/src/index.d.ts +8 -0
- package/dist/src/proto/emulator_controller_pb.d.ts +1 -0
- package/package.json +17 -17
- package/proto/emulator_controller.proto +746 -181
- package/.vscode/launch.json +0 -21
- package/.vscode/settings.json +0 -3
- package/CONTRIBUTING.md +0 -28
- package/Makefile +0 -123
- package/android-emulator-webrtc.code-workspace +0 -7
- package/api_descriptor.pb +0 -0
- package/babel.config.js +0 -16
- package/cloudbuild.yaml +0 -41
- package/cloudbuilders/.gcloudignore +0 -1
- package/cloudbuilders/Dockerfile +0 -20
- package/cloudbuilders/README.md +0 -29
- package/cloudbuilders/cloudbuild.yaml +0 -19
- package/dist/components/emulator/net/jsep_protocol_driver.js +0 -365
- package/dist/components/emulator/views/simple_png_view.js +0 -110
- package/dist/proto/emulator_controller_grpc_web_pb.js +0 -1601
- package/dist/proto/emulator_web_client.js +0 -176
- package/dist/proto/rtc_service_grpc_web_pb.js +0 -231
- package/dist/proto/rtc_service_pb.js +0 -338
- package/emulator/index.js +0 -1
- package/eslint_prefix.py +0 -37
- package/gen_md_doc.js +0 -40
- package/npmrc.enc +0 -0
- package/proto/rtc_service.proto +0 -117
- package/protoc-plugin/Makefile +0 -31
- package/protoc-plugin/grpc_generator.cc +0 -1755
- package/src/components/emulator/emulator.js +0 -217
- package/src/components/emulator/net/emulator_status.js +0 -94
- package/src/components/emulator/net/jsep_protocol_driver.js +0 -364
- package/src/components/emulator/net/logcat.js +0 -155
- package/src/components/emulator/views/event_handler.js +0 -268
- package/src/components/emulator/views/simple_png_view.js +0 -105
- package/src/components/emulator/views/webrtc_view.js +0 -142
- package/src/index.js +0 -20
- package/src/proto/emulator_web_client.js +0 -140
- package/test/cloudbuild.yaml +0 -41
- package/test/emulator.test.js +0 -125
- package/test/event_handler.test.js +0 -91
- package/test/fake_events.js +0 -50
- package/test/jsep_protocol_driver.test.js +0 -228
- package/test/simple_png_view.test.js +0 -145
- package/test/touch_event_handler.test.js +0 -118
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
|
|
4
|
+
Object.defineProperty(exports, "__esModule", {
|
|
5
|
+
value: true
|
|
6
|
+
});
|
|
7
|
+
exports["default"] = void 0;
|
|
8
|
+
var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
|
|
9
|
+
var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
|
|
10
|
+
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
|
|
11
|
+
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
|
|
12
|
+
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
|
|
13
|
+
var _logger = _interopRequireDefault(require("./logger"));
|
|
14
|
+
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
|
|
15
|
+
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { (0, _defineProperty2["default"])(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } /*
|
|
16
|
+
* Copyright 2026 The Android Open Source Project
|
|
17
|
+
*
|
|
18
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
19
|
+
* you may not use this file except in compliance with the License.
|
|
20
|
+
* You may obtain a copy of the License at
|
|
21
|
+
*
|
|
22
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
23
|
+
*
|
|
24
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
25
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
26
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
27
|
+
* See the License for the specific language governing permissions and
|
|
28
|
+
* limitations under the License.
|
|
29
|
+
*/ /*
|
|
30
|
+
* Copyright 2026 The Android Open Source Project
|
|
31
|
+
*
|
|
32
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
33
|
+
* you may not use this file except in compliance with the License.
|
|
34
|
+
* You may obtain a copy of the License at
|
|
35
|
+
*
|
|
36
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
37
|
+
*
|
|
38
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
39
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
40
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
41
|
+
* See the License for the specific language governing permissions and
|
|
42
|
+
* limitations under the License.
|
|
43
|
+
*/
|
|
44
|
+
/**
|
|
45
|
+
* A JSEP protocol driver that uses WebSockets for signaling.
|
|
46
|
+
*
|
|
47
|
+
* @export
|
|
48
|
+
* @class WsJsepProtocol
|
|
49
|
+
*/
|
|
50
|
+
var WsJsepProtocol = exports["default"] = /*#__PURE__*/function () {
|
|
51
|
+
/**
|
|
52
|
+
* Creates an instance of WsJsepProtocol.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} wsUrl The WebSocket JSEP signaling URL.
|
|
55
|
+
* @param {Object} [emulator=null] Fallback emulator controller for sending events when WebRTC is unavailable.
|
|
56
|
+
* @param {Object} [config={}] Configuration options.
|
|
57
|
+
* @param {boolean} [config.enableLogging=false] Whether verbose signaling logging is enabled.
|
|
58
|
+
* @param {function} [config.onError] Callback invoked when an error occurs.
|
|
59
|
+
* @param {number} [config.maxReconnectAttempts=5] Maximum number of reconnection attempts.
|
|
60
|
+
* @param {number} [config.reconnectDelay=1000] Initial delay for reconnection in milliseconds.
|
|
61
|
+
* @param {number} [config.reconnectBackoffFactor=2] Exponential backoff factor.
|
|
62
|
+
*/
|
|
63
|
+
function WsJsepProtocol(wsUrl) {
|
|
64
|
+
var _this = this,
|
|
65
|
+
_this$config$maxRecon,
|
|
66
|
+
_this$config$reconnec,
|
|
67
|
+
_this$config$reconnec2;
|
|
68
|
+
var emulator = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
|
|
69
|
+
var _config = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
|
|
70
|
+
(0, _classCallCheck2["default"])(this, WsJsepProtocol);
|
|
71
|
+
/**
|
|
72
|
+
* Establishes the WebSocket connection and starts the signaling process.
|
|
73
|
+
* Cleans up any existing connection beforehand.
|
|
74
|
+
*
|
|
75
|
+
* @param {Object} [callbacks] Callbacks for stream lifecycle events.
|
|
76
|
+
* @param {function(MediaStreamTrack): void} [callbacks.onConnected] Called when a media track is received.
|
|
77
|
+
* @param {function(): void} [callbacks.onDisconnected] Called when the stream is disconnected.
|
|
78
|
+
*/
|
|
79
|
+
(0, _defineProperty2["default"])(this, "startStream", function () {
|
|
80
|
+
var callbacks = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
|
81
|
+
_this.cleanup();
|
|
82
|
+
_this.reconnectAttempts = 0;
|
|
83
|
+
_this.onConnected = callbacks.onConnected || _this.onConnected;
|
|
84
|
+
_this.onDisconnected = callbacks.onDisconnected || _this.onDisconnected;
|
|
85
|
+
_this.connected = true;
|
|
86
|
+
_this._connect();
|
|
87
|
+
});
|
|
88
|
+
/**
|
|
89
|
+
* Internal method to establish WebSocket connection.
|
|
90
|
+
*
|
|
91
|
+
* @private
|
|
92
|
+
*/
|
|
93
|
+
(0, _defineProperty2["default"])(this, "_connect", function () {
|
|
94
|
+
if (_this.reconnectTimeoutId) {
|
|
95
|
+
clearTimeout(_this.reconnectTimeoutId);
|
|
96
|
+
_this.reconnectTimeoutId = null;
|
|
97
|
+
}
|
|
98
|
+
_this.ws = new WebSocket(_this.wsUrl);
|
|
99
|
+
_this.ws.onmessage = _this._handleWsMessage;
|
|
100
|
+
_this.ws.onclose = _this._handleWsClose;
|
|
101
|
+
_this.ws.onerror = _this._handleWsError;
|
|
102
|
+
});
|
|
103
|
+
/**
|
|
104
|
+
* Queues a reconnection attempt with exponential backoff.
|
|
105
|
+
*
|
|
106
|
+
* @private
|
|
107
|
+
*/
|
|
108
|
+
(0, _defineProperty2["default"])(this, "_queueReconnect", function () {
|
|
109
|
+
if (_this.reconnectTimeoutId) return;
|
|
110
|
+
_this.reconnectAttempts++;
|
|
111
|
+
if (_this.reconnectAttempts > _this.maxReconnectAttempts) {
|
|
112
|
+
_logger["default"].error("Max reconnect attempts (".concat(_this.maxReconnectAttempts, ") reached. Giving up."));
|
|
113
|
+
if (_this.onError) {
|
|
114
|
+
_this.onError(new Error("Connection failed: Max reconnect attempts reached."));
|
|
115
|
+
}
|
|
116
|
+
_this.disconnect();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
var delay = _this.reconnectDelay * Math.pow(_this.reconnectBackoffFactor, _this.reconnectAttempts - 1);
|
|
120
|
+
_logger["default"].info("Queueing reconnect attempt ".concat(_this.reconnectAttempts, " in ").concat(delay, "ms"));
|
|
121
|
+
_this.reconnectTimeoutId = setTimeout(function () {
|
|
122
|
+
_this.reconnectTimeoutId = null;
|
|
123
|
+
// Partially disconnect to clean up the failed peer connection/websocket,
|
|
124
|
+
// but keep this.connected = true so we know we want to reconnect.
|
|
125
|
+
_this._disconnectState();
|
|
126
|
+
_this._connect();
|
|
127
|
+
}, delay);
|
|
128
|
+
});
|
|
129
|
+
/**
|
|
130
|
+
* Internal handler for incoming WebSocket messages. Parses the signal
|
|
131
|
+
* and queues it for sequential processing.
|
|
132
|
+
*
|
|
133
|
+
* @private
|
|
134
|
+
* @param {MessageEvent} event The WebSocket message event.
|
|
135
|
+
*/
|
|
136
|
+
(0, _defineProperty2["default"])(this, "_handleWsMessage", function (event) {
|
|
137
|
+
try {
|
|
138
|
+
var signal = JSON.parse(event.data);
|
|
139
|
+
// Push to queue instead of handling immediately
|
|
140
|
+
_this.signalQueue.push(signal);
|
|
141
|
+
_this._processSignalQueue();
|
|
142
|
+
} catch (e) {
|
|
143
|
+
_logger["default"].error("Failed to handle WS message:", e, "Raw payload:", event.data);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
/**
|
|
147
|
+
* Sequentially processes JSEP signals from the queue.
|
|
148
|
+
*
|
|
149
|
+
* @private
|
|
150
|
+
*/
|
|
151
|
+
(0, _defineProperty2["default"])(this, "_processSignalQueue", /*#__PURE__*/(0, _asyncToGenerator2["default"])(/*#__PURE__*/_regenerator["default"].mark(function _callee() {
|
|
152
|
+
var signal;
|
|
153
|
+
return _regenerator["default"].wrap(function _callee$(_context) {
|
|
154
|
+
while (1) switch (_context.prev = _context.next) {
|
|
155
|
+
case 0:
|
|
156
|
+
if (!_this.isProcessingSignal) {
|
|
157
|
+
_context.next = 2;
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
return _context.abrupt("return");
|
|
161
|
+
case 2:
|
|
162
|
+
_this.isProcessingSignal = true;
|
|
163
|
+
case 3:
|
|
164
|
+
if (!(_this.signalQueue.length > 0)) {
|
|
165
|
+
_context.next = 9;
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
signal = _this.signalQueue.shift();
|
|
169
|
+
_context.next = 7;
|
|
170
|
+
return _this._handleSignal(signal);
|
|
171
|
+
case 7:
|
|
172
|
+
_context.next = 3;
|
|
173
|
+
break;
|
|
174
|
+
case 9:
|
|
175
|
+
_this.isProcessingSignal = false;
|
|
176
|
+
case 10:
|
|
177
|
+
case "end":
|
|
178
|
+
return _context.stop();
|
|
179
|
+
}
|
|
180
|
+
}, _callee);
|
|
181
|
+
})));
|
|
182
|
+
/**
|
|
183
|
+
* Handles WebSocket connection close events.
|
|
184
|
+
*
|
|
185
|
+
* @private
|
|
186
|
+
* @param {CloseEvent} event The WebSocket close event.
|
|
187
|
+
*/
|
|
188
|
+
(0, _defineProperty2["default"])(this, "_handleWsClose", function (event) {
|
|
189
|
+
_logger["default"].debug("WebSocket closed:", event);
|
|
190
|
+
if (_this.connected) {
|
|
191
|
+
_this._queueReconnect();
|
|
192
|
+
} else {
|
|
193
|
+
_this.disconnect();
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
/**
|
|
197
|
+
* Handles WebSocket error events.
|
|
198
|
+
*
|
|
199
|
+
* @private
|
|
200
|
+
* @param {Event} error The WebSocket error event.
|
|
201
|
+
*/
|
|
202
|
+
(0, _defineProperty2["default"])(this, "_handleWsError", function (error) {
|
|
203
|
+
_logger["default"].error("WebSocket error:", error);
|
|
204
|
+
if (_this.connected) {
|
|
205
|
+
_this._queueReconnect();
|
|
206
|
+
} else {
|
|
207
|
+
if (_this.onError) {
|
|
208
|
+
_this.onError(error);
|
|
209
|
+
}
|
|
210
|
+
_this.disconnect();
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
/**
|
|
214
|
+
* Processes a single JSEP signal (e.g., start, offer, answer, candidate, bye).
|
|
215
|
+
*
|
|
216
|
+
* @private
|
|
217
|
+
* @param {Object} signal The JSEP signaling message.
|
|
218
|
+
*/
|
|
219
|
+
(0, _defineProperty2["default"])(this, "_handleSignal", /*#__PURE__*/function () {
|
|
220
|
+
var _ref2 = (0, _asyncToGenerator2["default"])(/*#__PURE__*/_regenerator["default"].mark(function _callee2(signal) {
|
|
221
|
+
return _regenerator["default"].wrap(function _callee2$(_context2) {
|
|
222
|
+
while (1) switch (_context2.prev = _context2.next) {
|
|
223
|
+
case 0:
|
|
224
|
+
_logger["default"].debug("JSEP << [Received from Server]:", JSON.stringify(signal, null, 2));
|
|
225
|
+
_context2.prev = 1;
|
|
226
|
+
if (!signal.start) {
|
|
227
|
+
_context2.next = 5;
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
_context2.next = 5;
|
|
231
|
+
return _this._handleStart(signal.start);
|
|
232
|
+
case 5:
|
|
233
|
+
if (!(signal.type === "offer" || signal.type === "answer")) {
|
|
234
|
+
_context2.next = 10;
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
_context2.next = 8;
|
|
238
|
+
return _this._handleSDP(signal);
|
|
239
|
+
case 8:
|
|
240
|
+
_context2.next = 13;
|
|
241
|
+
break;
|
|
242
|
+
case 10:
|
|
243
|
+
if (!(signal.sdp && signal.sdp.type)) {
|
|
244
|
+
_context2.next = 13;
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
_context2.next = 13;
|
|
248
|
+
return _this._handleSDP(signal.sdp);
|
|
249
|
+
case 13:
|
|
250
|
+
if (signal.candidate) {
|
|
251
|
+
_this._handleCandidate(signal.candidate);
|
|
252
|
+
}
|
|
253
|
+
if (signal.bye) {
|
|
254
|
+
_this._handleBye();
|
|
255
|
+
}
|
|
256
|
+
_context2.next = 20;
|
|
257
|
+
break;
|
|
258
|
+
case 17:
|
|
259
|
+
_context2.prev = 17;
|
|
260
|
+
_context2.t0 = _context2["catch"](1);
|
|
261
|
+
_logger["default"].error("Error processing signal:", _context2.t0);
|
|
262
|
+
case 20:
|
|
263
|
+
case "end":
|
|
264
|
+
return _context2.stop();
|
|
265
|
+
}
|
|
266
|
+
}, _callee2, null, [[1, 17]]);
|
|
267
|
+
}));
|
|
268
|
+
return function (_x) {
|
|
269
|
+
return _ref2.apply(this, arguments);
|
|
270
|
+
};
|
|
271
|
+
}());
|
|
272
|
+
/**
|
|
273
|
+
* Initializes the RTCPeerConnection and local data channels based on the start configuration.
|
|
274
|
+
*
|
|
275
|
+
* @private
|
|
276
|
+
* @param {Object} config The signaling start configuration.
|
|
277
|
+
*/
|
|
278
|
+
(0, _defineProperty2["default"])(this, "_handleStart", /*#__PURE__*/function () {
|
|
279
|
+
var _ref3 = (0, _asyncToGenerator2["default"])(/*#__PURE__*/_regenerator["default"].mark(function _callee3(config) {
|
|
280
|
+
var localOnlyConfig, mouse, keyboard, touch, offer;
|
|
281
|
+
return _regenerator["default"].wrap(function _callee3$(_context3) {
|
|
282
|
+
while (1) switch (_context3.prev = _context3.next) {
|
|
283
|
+
case 0:
|
|
284
|
+
localOnlyConfig = _objectSpread(_objectSpread({}, config), {}, {
|
|
285
|
+
iceServers: []
|
|
286
|
+
});
|
|
287
|
+
_this.peerConnection = new RTCPeerConnection(localOnlyConfig);
|
|
288
|
+
_this.peerConnection.ontrack = _this._handlePeerConnectionTrack;
|
|
289
|
+
_this.peerConnection.onicecandidate = _this._handlePeerIceCandidate;
|
|
290
|
+
_this.peerConnection.onconnectionstatechange = _this._handlePeerConnectionStateChange;
|
|
291
|
+
_this.peerConnection.ondatachannel = _this._handleDataChannel;
|
|
292
|
+
_this.peerConnection.addTransceiver("video", {
|
|
293
|
+
direction: "recvonly"
|
|
294
|
+
});
|
|
295
|
+
_this.peerConnection.addTransceiver("audio", {
|
|
296
|
+
direction: "recvonly"
|
|
297
|
+
});
|
|
298
|
+
mouse = _this.peerConnection.createDataChannel("mouse");
|
|
299
|
+
keyboard = _this.peerConnection.createDataChannel("keyboard");
|
|
300
|
+
touch = _this.peerConnection.createDataChannel("touch");
|
|
301
|
+
_this._setupDataChannel(mouse);
|
|
302
|
+
_this._setupDataChannel(keyboard);
|
|
303
|
+
_this._setupDataChannel(touch);
|
|
304
|
+
_this.connected = true;
|
|
305
|
+
_context3.prev = 15;
|
|
306
|
+
_context3.next = 18;
|
|
307
|
+
return _this.peerConnection.createOffer();
|
|
308
|
+
case 18:
|
|
309
|
+
offer = _context3.sent;
|
|
310
|
+
if (_this.peerConnection) {
|
|
311
|
+
_context3.next = 21;
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
return _context3.abrupt("return");
|
|
315
|
+
case 21:
|
|
316
|
+
_context3.next = 23;
|
|
317
|
+
return _this.peerConnection.setLocalDescription(offer);
|
|
318
|
+
case 23:
|
|
319
|
+
if (_this.peerConnection) {
|
|
320
|
+
_context3.next = 25;
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
return _context3.abrupt("return");
|
|
324
|
+
case 25:
|
|
325
|
+
_this._sendJsep({
|
|
326
|
+
sdp: offer
|
|
327
|
+
});
|
|
328
|
+
_context3.next = 31;
|
|
329
|
+
break;
|
|
330
|
+
case 28:
|
|
331
|
+
_context3.prev = 28;
|
|
332
|
+
_context3.t0 = _context3["catch"](15);
|
|
333
|
+
_logger["default"].error("Failed to create or set local offer:", _context3.t0);
|
|
334
|
+
case 31:
|
|
335
|
+
case "end":
|
|
336
|
+
return _context3.stop();
|
|
337
|
+
}
|
|
338
|
+
}, _callee3, null, [[15, 28]]);
|
|
339
|
+
}));
|
|
340
|
+
return function (_x2) {
|
|
341
|
+
return _ref3.apply(this, arguments);
|
|
342
|
+
};
|
|
343
|
+
}());
|
|
344
|
+
/**
|
|
345
|
+
* Handles incoming media track events from the RTCPeerConnection.
|
|
346
|
+
*
|
|
347
|
+
* @private
|
|
348
|
+
* @param {RTCTrackEvent} e The track event.
|
|
349
|
+
*/
|
|
350
|
+
(0, _defineProperty2["default"])(this, "_handlePeerConnectionTrack", function (e) {
|
|
351
|
+
if (_this.onConnected) {
|
|
352
|
+
_this.onConnected(e.track);
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
/**
|
|
356
|
+
* Handles ICE candidate generation from the local RTCPeerConnection.
|
|
357
|
+
*
|
|
358
|
+
* @private
|
|
359
|
+
* @param {RTCPeerConnectionIceEvent} e The ICE candidate event.
|
|
360
|
+
*/
|
|
361
|
+
(0, _defineProperty2["default"])(this, "_handlePeerIceCandidate", function (e) {
|
|
362
|
+
if (e.candidate === null) return;
|
|
363
|
+
_this._sendJsep({
|
|
364
|
+
candidate: e.candidate
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
/**
|
|
368
|
+
* Monitors connection state changes on the RTCPeerConnection to trigger disconnection.
|
|
369
|
+
*
|
|
370
|
+
* @private
|
|
371
|
+
* @param {Event} e The state change event.
|
|
372
|
+
*/
|
|
373
|
+
(0, _defineProperty2["default"])(this, "_handlePeerConnectionStateChange", function (e) {
|
|
374
|
+
if (!_this.peerConnection) return;
|
|
375
|
+
switch (_this.peerConnection.connectionState) {
|
|
376
|
+
case "disconnected":
|
|
377
|
+
case "failed":
|
|
378
|
+
case "closed":
|
|
379
|
+
_this.disconnect();
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
/**
|
|
383
|
+
* Registers a data channel for event forwarding.
|
|
384
|
+
*
|
|
385
|
+
* @private
|
|
386
|
+
* @param {RTCDataChannel} channel The data channel.
|
|
387
|
+
*/
|
|
388
|
+
(0, _defineProperty2["default"])(this, "_setupDataChannel", function (channel) {
|
|
389
|
+
_this.event_forwarders[channel.label] = channel;
|
|
390
|
+
});
|
|
391
|
+
/**
|
|
392
|
+
* Handles remote data channel creation.
|
|
393
|
+
*
|
|
394
|
+
* @private
|
|
395
|
+
* @param {RTCDataChannelEvent} e The data channel event.
|
|
396
|
+
*/
|
|
397
|
+
(0, _defineProperty2["default"])(this, "_handleDataChannel", function (e) {
|
|
398
|
+
_this._setupDataChannel(e.channel);
|
|
399
|
+
});
|
|
400
|
+
/**
|
|
401
|
+
* Processes a remote SDP offer or answer, applying it to the RTCPeerConnection.
|
|
402
|
+
*
|
|
403
|
+
* @private
|
|
404
|
+
* @param {RTCSessionDescriptionInit} sdp The session description.
|
|
405
|
+
*/
|
|
406
|
+
(0, _defineProperty2["default"])(this, "_handleSDP", /*#__PURE__*/function () {
|
|
407
|
+
var _ref4 = (0, _asyncToGenerator2["default"])(/*#__PURE__*/_regenerator["default"].mark(function _callee4(sdp) {
|
|
408
|
+
var candidate, answer;
|
|
409
|
+
return _regenerator["default"].wrap(function _callee4$(_context4) {
|
|
410
|
+
while (1) switch (_context4.prev = _context4.next) {
|
|
411
|
+
case 0:
|
|
412
|
+
if (_this.peerConnection) {
|
|
413
|
+
_context4.next = 2;
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
return _context4.abrupt("return");
|
|
417
|
+
case 2:
|
|
418
|
+
_context4.prev = 2;
|
|
419
|
+
_context4.next = 5;
|
|
420
|
+
return _this.peerConnection.setRemoteDescription(new RTCSessionDescription(sdp));
|
|
421
|
+
case 5:
|
|
422
|
+
if (_this.peerConnection) {
|
|
423
|
+
_context4.next = 7;
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
426
|
+
return _context4.abrupt("return");
|
|
427
|
+
case 7:
|
|
428
|
+
_this.remoteDescriptionSet = true;
|
|
429
|
+
_logger["default"].debug("Processing ".concat(_this.pendingCandidates.length, " queued ICE candidates."));
|
|
430
|
+
while (_this.pendingCandidates.length > 0) {
|
|
431
|
+
candidate = _this.pendingCandidates.shift();
|
|
432
|
+
_this._addIceCandidate(candidate);
|
|
433
|
+
}
|
|
434
|
+
if (!(sdp.type === "offer")) {
|
|
435
|
+
_context4.next = 21;
|
|
436
|
+
break;
|
|
437
|
+
}
|
|
438
|
+
_context4.next = 13;
|
|
439
|
+
return _this.peerConnection.createAnswer();
|
|
440
|
+
case 13:
|
|
441
|
+
answer = _context4.sent;
|
|
442
|
+
if (_this.peerConnection) {
|
|
443
|
+
_context4.next = 16;
|
|
444
|
+
break;
|
|
445
|
+
}
|
|
446
|
+
return _context4.abrupt("return");
|
|
447
|
+
case 16:
|
|
448
|
+
_context4.next = 18;
|
|
449
|
+
return _this.peerConnection.setLocalDescription(answer);
|
|
450
|
+
case 18:
|
|
451
|
+
if (_this.peerConnection) {
|
|
452
|
+
_context4.next = 20;
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
455
|
+
return _context4.abrupt("return");
|
|
456
|
+
case 20:
|
|
457
|
+
_this._sendJsep({
|
|
458
|
+
sdp: answer
|
|
459
|
+
});
|
|
460
|
+
case 21:
|
|
461
|
+
_context4.next = 26;
|
|
462
|
+
break;
|
|
463
|
+
case 23:
|
|
464
|
+
_context4.prev = 23;
|
|
465
|
+
_context4.t0 = _context4["catch"](2);
|
|
466
|
+
_logger["default"].error("Failed to process remote SDP:", _context4.t0);
|
|
467
|
+
case 26:
|
|
468
|
+
case "end":
|
|
469
|
+
return _context4.stop();
|
|
470
|
+
}
|
|
471
|
+
}, _callee4, null, [[2, 23]]);
|
|
472
|
+
}));
|
|
473
|
+
return function (_x3) {
|
|
474
|
+
return _ref4.apply(this, arguments);
|
|
475
|
+
};
|
|
476
|
+
}());
|
|
477
|
+
/**
|
|
478
|
+
* Adds a remote ICE candidate to the RTCPeerConnection.
|
|
479
|
+
*
|
|
480
|
+
* @private
|
|
481
|
+
* @param {RTCIceCandidateInit|string} candidate The ICE candidate object or string.
|
|
482
|
+
*/
|
|
483
|
+
(0, _defineProperty2["default"])(this, "_addIceCandidate", function (candidate) {
|
|
484
|
+
try {
|
|
485
|
+
var candidateInit = typeof candidate === 'string' ? {
|
|
486
|
+
candidate: candidate,
|
|
487
|
+
sdpMid: "0",
|
|
488
|
+
sdpMLineIndex: 0
|
|
489
|
+
} : candidate;
|
|
490
|
+
_this.peerConnection.addIceCandidate(new RTCIceCandidate(candidateInit));
|
|
491
|
+
} catch (e) {
|
|
492
|
+
_logger["default"].warn("Failed to add ICE candidate:", e, candidate);
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
/**
|
|
496
|
+
* Handles an incoming remote ICE candidate, queueing it if the remote description is not yet set.
|
|
497
|
+
*
|
|
498
|
+
* @private
|
|
499
|
+
* @param {RTCIceCandidateInit} candidate The remote ICE candidate.
|
|
500
|
+
*/
|
|
501
|
+
(0, _defineProperty2["default"])(this, "_handleCandidate", function (candidate) {
|
|
502
|
+
if (!_this.peerConnection) return;
|
|
503
|
+
if (!_this.remoteDescriptionSet) {
|
|
504
|
+
_logger["default"].debug("Queueing ICE candidate until remote description is set:", candidate);
|
|
505
|
+
_this.pendingCandidates.push(candidate);
|
|
506
|
+
} else {
|
|
507
|
+
_this._addIceCandidate(candidate);
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
/**
|
|
511
|
+
* Handles the 'bye' signal from the remote side, triggering disconnection.
|
|
512
|
+
*
|
|
513
|
+
* @private
|
|
514
|
+
*/
|
|
515
|
+
(0, _defineProperty2["default"])(this, "_handleBye", function () {
|
|
516
|
+
_this.disconnect();
|
|
517
|
+
});
|
|
518
|
+
/**
|
|
519
|
+
* Serializes and sends a JSEP JSON message over the WebSocket.
|
|
520
|
+
*
|
|
521
|
+
* @private
|
|
522
|
+
* @param {Object} jsonObject The JSON payload.
|
|
523
|
+
*/
|
|
524
|
+
(0, _defineProperty2["default"])(this, "_sendJsep", function (jsonObject) {
|
|
525
|
+
_logger["default"].debug("JSEP >> [Sending to Server]:", JSON.stringify(jsonObject, null, 2));
|
|
526
|
+
if (_this.ws && _this.ws.readyState === WebSocket.OPEN) {
|
|
527
|
+
_this.ws.send(JSON.stringify(jsonObject));
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
/**
|
|
531
|
+
* Cleans up the current connection's WebSocket and PeerConnection state,
|
|
532
|
+
* but does not mark the driver as permanently disconnected or trigger
|
|
533
|
+
* the onDisconnected callback. Used during reconnection.
|
|
534
|
+
*
|
|
535
|
+
* @private
|
|
536
|
+
*/
|
|
537
|
+
(0, _defineProperty2["default"])(this, "_disconnectState", function () {
|
|
538
|
+
_this.signalQueue = [];
|
|
539
|
+
_this.isProcessingSignal = false;
|
|
540
|
+
if (_this.ws) {
|
|
541
|
+
_this.ws.onclose = null;
|
|
542
|
+
_this.ws.onerror = null;
|
|
543
|
+
_this.ws.onmessage = null;
|
|
544
|
+
_this.ws.close();
|
|
545
|
+
_this.ws = null;
|
|
546
|
+
}
|
|
547
|
+
if (_this.peerConnection) {
|
|
548
|
+
_this.peerConnection.close();
|
|
549
|
+
_this.peerConnection = null;
|
|
550
|
+
}
|
|
551
|
+
_this.pendingCandidates = [];
|
|
552
|
+
_this.remoteDescriptionSet = false;
|
|
553
|
+
_this.event_forwarders = {};
|
|
554
|
+
});
|
|
555
|
+
/**
|
|
556
|
+
* Disconnects both the WebSocket signaling connection and the WebRTC PeerConnection.
|
|
557
|
+
*/
|
|
558
|
+
(0, _defineProperty2["default"])(this, "disconnect", function () {
|
|
559
|
+
_this.connected = false;
|
|
560
|
+
if (_this.reconnectTimeoutId) {
|
|
561
|
+
clearTimeout(_this.reconnectTimeoutId);
|
|
562
|
+
_this.reconnectTimeoutId = null;
|
|
563
|
+
}
|
|
564
|
+
_this.reconnectAttempts = 0;
|
|
565
|
+
_this._disconnectState();
|
|
566
|
+
if (_this.onDisconnected) {
|
|
567
|
+
_this.onDisconnected(_this);
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
/**
|
|
571
|
+
* Fully cleans up signaling and WebRTC state.
|
|
572
|
+
*/
|
|
573
|
+
(0, _defineProperty2["default"])(this, "cleanup", function () {
|
|
574
|
+
_this.disconnect();
|
|
575
|
+
_this.event_forwarders = {};
|
|
576
|
+
});
|
|
577
|
+
this.wsUrl = wsUrl;
|
|
578
|
+
this.emulator = emulator;
|
|
579
|
+
this.config = _objectSpread({
|
|
580
|
+
enableLogging: false
|
|
581
|
+
}, _config);
|
|
582
|
+
this.onError = this.config.onError;
|
|
583
|
+
this.maxReconnectAttempts = (_this$config$maxRecon = this.config.maxReconnectAttempts) !== null && _this$config$maxRecon !== void 0 ? _this$config$maxRecon : 5;
|
|
584
|
+
this.reconnectDelay = (_this$config$reconnec = this.config.reconnectDelay) !== null && _this$config$reconnec !== void 0 ? _this$config$reconnec : 1000;
|
|
585
|
+
this.reconnectBackoffFactor = (_this$config$reconnec2 = this.config.reconnectBackoffFactor) !== null && _this$config$reconnec2 !== void 0 ? _this$config$reconnec2 : 2;
|
|
586
|
+
this.connected = false;
|
|
587
|
+
this.event_forwarders = {};
|
|
588
|
+
this.peerConnection = null;
|
|
589
|
+
this.ws = null;
|
|
590
|
+
|
|
591
|
+
// WebRTC signaling state
|
|
592
|
+
this.pendingCandidates = [];
|
|
593
|
+
this.remoteDescriptionSet = false;
|
|
594
|
+
|
|
595
|
+
// Signaling message queue to prevent concurrent mutations
|
|
596
|
+
this.signalQueue = [];
|
|
597
|
+
this.isProcessingSignal = false;
|
|
598
|
+
|
|
599
|
+
// Callbacks set during startStream
|
|
600
|
+
this.onConnected = null;
|
|
601
|
+
this.onDisconnected = null;
|
|
602
|
+
|
|
603
|
+
// Reconnection state
|
|
604
|
+
this.reconnectAttempts = 0;
|
|
605
|
+
this.reconnectTimeoutId = null;
|
|
606
|
+
}
|
|
607
|
+
(0, _createClass2["default"])(WsJsepProtocol, [{
|
|
608
|
+
key: "send",
|
|
609
|
+
value:
|
|
610
|
+
/**
|
|
611
|
+
* Sends a control message (mouse, keyboard, touch) over either the corresponding
|
|
612
|
+
* WebRTC DataChannel or via the fallback emulator controller.
|
|
613
|
+
*
|
|
614
|
+
* @param {string} label The channel label ("mouse", "keyboard", "touch").
|
|
615
|
+
* @param {Object} msg The protobuf message instance.
|
|
616
|
+
*/
|
|
617
|
+
function send(label, msg) {
|
|
618
|
+
var bytes = msg.serializeBinary();
|
|
619
|
+
var forwarder = this.event_forwarders[label];
|
|
620
|
+
if (this.connected && forwarder && forwarder.readyState === "open") {
|
|
621
|
+
forwarder.send(bytes);
|
|
622
|
+
} else if (this.emulator) {
|
|
623
|
+
switch (label) {
|
|
624
|
+
case "mouse":
|
|
625
|
+
this.emulator.sendMouse(msg);
|
|
626
|
+
break;
|
|
627
|
+
case "keyboard":
|
|
628
|
+
this.emulator.sendKey(msg);
|
|
629
|
+
break;
|
|
630
|
+
case "touch":
|
|
631
|
+
this.emulator.sendTouch(msg);
|
|
632
|
+
break;
|
|
633
|
+
}
|
|
634
|
+
} else {
|
|
635
|
+
_logger["default"].warn("Data channel '".concat(label, "' is not open. Event was dropped."));
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}]);
|
|
639
|
+
return WsJsepProtocol;
|
|
640
|
+
}();
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import WsJsepProtocol from "../net/ws_jsep_protocol_driver";
|
|
3
|
+
export interface MouseKeyHandlerProps {
|
|
4
|
+
/** The REST endpoint to retrieve status. */
|
|
5
|
+
statusUrl: string;
|
|
6
|
+
/** Jsep protocol driver, used to send mouse & touch events. */
|
|
7
|
+
jsep: WsJsepProtocol;
|
|
8
|
+
/** The authentication service to use. */
|
|
9
|
+
auth?: any;
|
|
10
|
+
/** The width of the component. */
|
|
11
|
+
width?: number;
|
|
12
|
+
/** The height of the component. */
|
|
13
|
+
height?: number;
|
|
14
|
+
[key: string]: any;
|
|
15
|
+
}
|
|
16
|
+
export interface MouseKeyHandlerRef {
|
|
17
|
+
scaleCoordinates(xp: number, yp: number): {
|
|
18
|
+
x: number;
|
|
19
|
+
y: number;
|
|
20
|
+
scaleX: number;
|
|
21
|
+
scaleY: number;
|
|
22
|
+
};
|
|
23
|
+
setDeviceWidth: React.Dispatch<React.SetStateAction<number>>;
|
|
24
|
+
setDeviceHeight: React.Dispatch<React.SetStateAction<number>>;
|
|
25
|
+
handlerRef: React.RefObject<HTMLDivElement>;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* A handler that extends a view to send key/mouse events to the emulator.
|
|
29
|
+
* It wraps the inner component in a div, and will use the jsep handler
|
|
30
|
+
* to send key/mouse/touch events over the proper channel.
|
|
31
|
+
*
|
|
32
|
+
* It will translate the mouse events based upon the returned display size of
|
|
33
|
+
* the emulator.
|
|
34
|
+
*
|
|
35
|
+
* You usually want to wrap a EmulatorRtcview, or EmulatorPngView in it.
|
|
36
|
+
*/
|
|
37
|
+
export default function withMouseKeyHandler<P extends object>(WrappedComponent: React.ComponentType<P>): React.ForwardRefExoticComponent<React.PropsWithoutRef<MouseKeyHandlerProps & P> & React.RefAttributes<MouseKeyHandlerRef>>;
|