model-action 2.0.0 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/index.d.ts +7 -2
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +6 -2
  4. package/dist/index.js.map +1 -1
  5. package/dist/modules/Action.d.ts.map +1 -1
  6. package/dist/modules/Action.js +1 -0
  7. package/dist/modules/Action.js.map +1 -1
  8. package/dist/modules/Animation.d.ts +59 -0
  9. package/dist/modules/Animation.d.ts.map +1 -0
  10. package/dist/modules/Animation.js +117 -0
  11. package/dist/modules/Animation.js.map +1 -0
  12. package/dist/modules/CameraView.d.ts +37 -0
  13. package/dist/modules/CameraView.d.ts.map +1 -0
  14. package/dist/modules/CameraView.js +110 -0
  15. package/dist/modules/CameraView.js.map +1 -0
  16. package/dist/modules/Effect.d.ts +37 -0
  17. package/dist/modules/Effect.d.ts.map +1 -0
  18. package/dist/modules/Effect.js +72 -0
  19. package/dist/modules/Effect.js.map +1 -0
  20. package/dist/modules/GameMode.d.ts +17 -0
  21. package/dist/modules/GameMode.d.ts.map +1 -0
  22. package/dist/modules/GameMode.js +36 -0
  23. package/dist/modules/GameMode.js.map +1 -0
  24. package/dist/modules/Init.d.ts +28 -0
  25. package/dist/modules/Init.d.ts.map +1 -0
  26. package/dist/modules/Init.js +81 -0
  27. package/dist/modules/Init.js.map +1 -0
  28. package/dist/modules/Path.d.ts +79 -0
  29. package/dist/modules/Path.d.ts.map +1 -0
  30. package/dist/modules/Path.js +157 -0
  31. package/dist/modules/Path.js.map +1 -0
  32. package/dist/peer-stream.d.ts +123 -0
  33. package/dist/peer-stream.d.ts.map +1 -0
  34. package/dist/peer-stream.js +852 -0
  35. package/dist/peer-stream.js.map +1 -0
  36. package/package.json +1 -1
  37. package/src/index.ts +7 -2
  38. package/src/modules/Action.ts +1 -0
  39. package/src/modules/Animation.ts +171 -0
  40. package/src/modules/{Camera.ts → CameraView.ts} +2 -2
  41. package/src/modules/Effect.ts +127 -0
  42. package/src/modules/GameMode.ts +60 -0
  43. package/src/modules/Init.ts +104 -0
  44. package/src/modules/{Track.ts → Path.ts} +28 -21
  45. package/src/peer-stream.js +938 -0
@@ -0,0 +1,938 @@
1
+ "5.1.3";
2
+
3
+ // Must be kept in sync with JavaScriptKeyCodeToFKey C++ array.
4
+ // special keycodes different from KeyboardEvent.keyCode
5
+ const SpecialKeyCodes = {
6
+ Backspace: 8,
7
+ ShiftLeft: 16,
8
+ ControlLeft: 17,
9
+ AltLeft: 18,
10
+ ShiftRight: 253,
11
+ ControlRight: 254,
12
+ AltRight: 255,
13
+ };
14
+
15
+ // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button
16
+ const MouseButton = {
17
+ MainButton: 0, // Left button.
18
+ AuxiliaryButton: 1, // Wheel button.
19
+ SecondaryButton: 2, // Right button.
20
+ FourthButton: 3, // Browser Back button.
21
+ FifthButton: 4, // Browser Forward button.
22
+ };
23
+
24
+ // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons#value
25
+ const MouseButtonsMask = {
26
+ 1: 0,
27
+ 2: 2,
28
+ 4: 1,
29
+ 8: 3,
30
+ 16: 4
31
+ }
32
+
33
+ // Must be kept in sync with PixelStreamingProtocol::EToClientMsg C++ enum.
34
+ const RECEIVE = {
35
+ QualityControlOwnership: 0,
36
+ Response: 1,
37
+ Command: 2,
38
+ FreezeFrame: 3,
39
+ UnfreezeFrame: 4,
40
+ VideoEncoderAvgQP: 5,
41
+ LatencyTest: 6,
42
+ InitialSettings: 7,
43
+ FileExtension: 8,
44
+ FileMimeType: 9,
45
+ FileContents: 10,
46
+ InputControlOwnership: 12,
47
+ CompositionStart: 64,
48
+ Protocol: 255
49
+ };
50
+
51
+ // Must be kept in sync with PixelStreamingProtocol::EToUE4Msg C++ enum.
52
+ const SEND = {
53
+ /*
54
+ * Control Messages. Range = 0..49.
55
+ */
56
+ IFrameRequest: 0,
57
+ RequestQualityControl: 1,
58
+ FpsRequest: 2,
59
+ AverageBitrateRequest: 3,
60
+ StartStreaming: 4,
61
+ StopStreaming: 5,
62
+ LatencyTest: 6,
63
+ RequestInitialSettings: 7,
64
+ /*
65
+ * Input Messages. Range = 50..89.
66
+ */
67
+
68
+ // Generic Input Messages. Range = 50..59.
69
+ UIInteraction: 50,
70
+ Command: 51,
71
+
72
+ // Keyboard Input Message. Range = 60..69.
73
+ KeyDown: 60,
74
+ KeyUp: 61,
75
+ KeyPress: 62,
76
+ FindFocus: 63,
77
+ CompositionEnd: 64,
78
+
79
+ // Mouse Input Messages. Range = 70..79.
80
+ MouseEnter: 70,
81
+ MouseLeave: 71,
82
+ MouseDown: 72,
83
+ MouseUp: 73,
84
+ MouseMove: 74,
85
+ MouseWheel: 75,
86
+
87
+ // Touch Input Messages. Range = 80..89.
88
+ TouchStart: 80,
89
+ TouchEnd: 81,
90
+ TouchMove: 82,
91
+
92
+ // Gamepad Input Messages. Range = 90..99
93
+ GamepadButtonPressed: 90,
94
+ GamepadButtonReleased: 91,
95
+ GamepadAnalog: 92,
96
+ };
97
+
98
+ let iceServers = undefined;
99
+
100
+ class PeerStream extends HTMLVideoElement {
101
+ constructor() {
102
+ super();
103
+
104
+ window.ps = this;
105
+
106
+ this.ws = { send() { }, close() { } }; // WebSocket
107
+ this.pc = { close() { } }; // RTCPeerConnection
108
+
109
+ this.setupVideo();
110
+ this.registerKeyboardEvents();
111
+ this.registerMouseHoverEvents();
112
+ this.registerFakeMouseEvents();
113
+
114
+ document.addEventListener(
115
+ "pointerlockchange",
116
+ () => {
117
+ if (document.pointerLockElement === this) {
118
+ this.registerPointerLockEvents();
119
+ } else {
120
+ this.registerMouseHoverEvents();
121
+ }
122
+ },
123
+ false
124
+ );
125
+
126
+ this.addEventListener("loadeddata", (e) => {
127
+ this.style["aspect-ratio"] = this.videoWidth / this.videoHeight;
128
+ });
129
+
130
+ // this.setupPeerConnection();
131
+ }
132
+ checkWebRTCSupport() {
133
+ // Step 2: Check for RTCPeerConnection
134
+ const RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection;
135
+ if (!RTCPeerConnection) {
136
+ console.warn('checkWebRTCSupport RTCPeerConnection not supported');
137
+ return false
138
+ }
139
+ // Step 3: Check for DataChannel
140
+ let dataChannelSupported = false;
141
+ let pc = null;
142
+ if (RTCPeerConnection) {
143
+ try {
144
+ pc = new RTCPeerConnection();
145
+ const dc = pc.createDataChannel('test');
146
+ dataChannelSupported = !!dc;
147
+ dc.close(); // Close the DataChannel when done
148
+ pc.close()
149
+ } catch (e) {
150
+ console.error(e)
151
+ console.warn('checkWebRTCSupport dataChannelSupported not supported');
152
+ return false
153
+ }
154
+ if (!dataChannelSupported) {
155
+ console.warn('checkWebRTCSupport DataChannel not supported');
156
+ return false
157
+ }
158
+ }
159
+ return true
160
+
161
+ }
162
+
163
+ // setupWebsocket
164
+ async connectedCallback() {
165
+ if (false == this.checkWebRTCSupport()) {
166
+ const overlayDiv = document.createElement('div');
167
+ overlayDiv.innerHTML = '你的浏览器版本过低!<br>推荐使用谷歌100以上版本浏览器!!';
168
+ overlayDiv.style.position = 'absolute';
169
+ overlayDiv.style.top = '50%';
170
+ overlayDiv.style.left = '50%';
171
+ overlayDiv.style.transform = 'translate(-50%, -50%)';
172
+ overlayDiv.style.background = 'rgba(255, 255, 255, 0.8)';
173
+ overlayDiv.style.padding = '10px';
174
+ overlayDiv.style.borderRadius = '5px';
175
+ overlayDiv.style.display = 'block'; // Initially hidden
176
+ this.parentNode.appendChild(overlayDiv)
177
+ }
178
+
179
+ // This will happen each time the node is moved, and may happen before the element"s contents have been fully parsed. may be called once your element is no longer connected
180
+ if (!this.isConnected) return;
181
+ if (this.pc.connectionState === "connected" && this.dc.readyState === "open" && this.ws.readyState === 1) {
182
+ // this.pc.restartIce();
183
+ this.play();
184
+ return;
185
+ }
186
+ // await new Promise((res) => setTimeout(res, 1000));
187
+ this.ws.onclose = null
188
+ this.ws.close(1000);
189
+ this.ws = new WebSocket(this.id || location.href.replace(/^http/, "ws"), 'peer-stream');
190
+
191
+ this.ws.onerror
192
+
193
+ this.ws.onopen = () => {
194
+ console.info("✅", this.ws);
195
+ };
196
+
197
+ this.ws.onmessage = (e) => {
198
+ this.onWebSocketMessage(e.data);
199
+ };
200
+
201
+ this.ws.onclose = (e) => {
202
+ console.warn(e);
203
+ this.dispatchEvent(new CustomEvent("playerdisconnected", {}));
204
+ clearTimeout(this.reconnect);
205
+ this.reconnect = setTimeout(() => this.connectedCallback(), 3000);
206
+ };
207
+ }
208
+
209
+ disconnectedCallback() {
210
+ // lifecycle binding
211
+ setTimeout(() => {
212
+ if (this.isConnected) return
213
+ this.ws.close(1000);
214
+ this.pc.close();
215
+ console.log("❌ peer connection closing");
216
+ // this.dc.close();
217
+ }, 5 * 1000);
218
+ }
219
+
220
+ adoptedCallback() { }
221
+
222
+ attributeChangedCallback(name, oldValue, newValue) {
223
+ if (!this.isConnected) return;
224
+ // fired before connectedCallback when startup
225
+ this.ws.close(1000);
226
+ }
227
+
228
+ async onWebSocketMessage(msg) {
229
+ try {
230
+ msg = JSON.parse(msg);
231
+ } catch {
232
+ console.debug("↓↓", msg);
233
+ return;
234
+ }
235
+ if (msg.type === "offer") {
236
+ this.setupPeerConnection();
237
+
238
+ const offer = new RTCSessionDescription(msg);
239
+ console.log("↓↓ offer", offer);
240
+
241
+ await this.pc.setRemoteDescription(offer);
242
+
243
+ // Setup a transceiver for getting UE video
244
+ this.pc.addTransceiver("video", { direction: "recvonly" });
245
+
246
+ const answer = await this.pc.createAnswer();
247
+ await this.pc.setLocalDescription(answer);
248
+
249
+ console.log("↑↑ answer", answer);
250
+ this.ws.send(JSON.stringify(answer));
251
+
252
+ for (let receiver of this.pc.getReceivers()) {
253
+ receiver.playoutDelayHint = 0;
254
+ }
255
+ } else if (msg.type === "iceCandidate") {
256
+ const candidate = new RTCIceCandidate(msg.candidate);
257
+ console.log("↓↓ candidate:", candidate);
258
+ await this.pc.addIceCandidate(candidate);
259
+ } else if (msg.type === "answer") {
260
+ const answer = new RTCSessionDescription(msg)
261
+ await this.pc.setRemoteDescription(answer)
262
+ console.log('↓↓ answer:', answer)
263
+ for (const receiver of this.pc.getReceivers()) {
264
+ receiver.playoutDelayHint = 0
265
+ }
266
+ } else if (msg.type === "playerqueue") {
267
+ this.dispatchEvent(new CustomEvent("playerqueue", { detail: msg }));
268
+ console.log("↓↓ playerqueue:", msg);
269
+ } else if (msg.type === "seticeServers") {
270
+ iceServers = msg.iceServers
271
+ console.log("↓↓ seticeServers:", msg);
272
+ } else if (msg.type === 'playerConnected') {
273
+ console.log('↓↓ playerConnected:', msg)
274
+ this.setupPeerConnection_ue4()
275
+ this.setupDataChannel_ue4()
276
+ } else if (msg.type === "ping") {
277
+ console.log("↓↓ ping:", msg);
278
+ msg.type = "pong"
279
+ this.ws.send(JSON.stringify(msg));
280
+
281
+ if (this.mouseReleaseTime) {
282
+ let now = new Date()
283
+ if ((now - this.lastmouseTime) > this.mouseReleaseTime * 1000) {
284
+ msg.type = "mouseRelease"
285
+ this.ws.send(JSON.stringify(msg));
286
+ }
287
+ }
288
+ }
289
+ else if (msg.type === "ueDisConnected") {
290
+ this.dispatchEvent(new CustomEvent("ueDisConnected", { detail: msg }));
291
+ console.log("↓↓ ueDisConnected:", msg);
292
+ }
293
+ else if (msg.type === "setmouseReleaseTime") {
294
+ this.mouseReleaseTime = msg.mouseReleaseTime
295
+ this.lastmouseTime = new Date()
296
+ console.log("↓↓ setmouseReleaseTime:", msg);
297
+ }
298
+ else if (msg.type === "getStatus") {
299
+ console.log("↓↓ getStatus:", msg);
300
+ this.handleGetStatus(msg)
301
+ }
302
+ else {
303
+ console.warn("↓↓", msg);
304
+ }
305
+ }
306
+ handleGetStatus(msg) {
307
+ if (false == (this.pc instanceof RTCPeerConnection)) {
308
+ msg.videoencoderqp = null
309
+ msg.netrate = null
310
+ this.ws.send(JSON.stringify(msg));
311
+ console.log("↑↑ handleGetStatus:", msg);
312
+ return
313
+ }
314
+ let initialBytesReceived = 0;
315
+ // 获取初始统计信息
316
+ this.pc.getStats(null).then(stats => {
317
+ stats.forEach(report => {
318
+ if (report.type === "transport") {
319
+ initialBytesReceived = report.bytesReceived;
320
+ }
321
+ });
322
+ });
323
+ // 等待指定的时间间隔后再次获取统计信息
324
+ let durationInSeconds = 0.2
325
+ setTimeout(() => {
326
+ this.pc.getStats(null).then(stats => {
327
+ stats.forEach(report => {
328
+ if (report.type === "transport") {
329
+ const finalBytesReceived = report.bytesReceived;
330
+ const bytesReceived = finalBytesReceived - initialBytesReceived;
331
+
332
+ // 计算平均带宽(单位:字节/秒)
333
+ const averageReceiveBandwidth = (bytesReceived / durationInSeconds)*8/1000/1000;
334
+ msg.videoencoderqp = this.VideoEncoderQP
335
+ msg.netrate = averageReceiveBandwidth.toFixed(2)
336
+ this.ws.send(JSON.stringify(msg));
337
+ console.log("↑↑ handleGetStatus:", msg);
338
+ }
339
+ });
340
+ });
341
+ }, durationInSeconds * 1000);
342
+ }
343
+
344
+ onDataChannelMessage(data) {
345
+ data = new Uint8Array(data);
346
+ const utf16 = new TextDecoder("utf-16");
347
+ switch (data[0]) {
348
+ case RECEIVE.VideoEncoderAvgQP: {
349
+ this.VideoEncoderQP = +utf16.decode(data.slice(1));
350
+ // console.debug("↓↓ QP:", this.VideoEncoderQP);
351
+ break;
352
+ }
353
+ case RECEIVE.Response: {
354
+ // user custom message
355
+ const detail = utf16.decode(data.slice(1));
356
+ this.dispatchEvent(new CustomEvent("message", { detail }));
357
+ console.info(detail);
358
+ break;
359
+ }
360
+ case RECEIVE.Command: {
361
+ const command = JSON.parse(utf16.decode(data.slice(1)));
362
+ console.info("↓↓ command:", command);
363
+ if (command.command === "onScreenKeyboard") {
364
+ console.info("You should setup a on-screen keyboard");
365
+ if (command.showOnScreenKeyboard) {
366
+ if (this.enableChinese) {
367
+ let input = document.createElement('input');
368
+ input.style.position = 'fixed';
369
+ input.style.zIndex = -1;
370
+ input.autofocus = true;
371
+ document.body.append(input);
372
+ input.focus();
373
+ input.addEventListener('compositionend', e => {
374
+ console.log(e.data)
375
+ this.emitMessage(e.data, SEND.CompositionEnd)
376
+ })
377
+ input.addEventListener('blue', e => {
378
+ input.remove()
379
+ })
380
+ input.addEventListener('keydown', e => {
381
+ this.onkeydown(e)
382
+ })
383
+ input.addEventListener('keyup', e => {
384
+ this.onkeyup(e)
385
+ })
386
+ input.addEventListener('keypress', e => {
387
+ this.onkeypress(e)
388
+ })
389
+ }
390
+ }
391
+ }
392
+ break;
393
+ }
394
+ case RECEIVE.FreezeFrame: {
395
+ const size = new DataView(data.slice(1, 5).buffer).getInt32(0, true);
396
+ const jpeg = data.slice(1 + 4);
397
+ console.info("↓↓ freezed frame:", jpeg);
398
+ break;
399
+ }
400
+ case RECEIVE.UnfreezeFrame: {
401
+ console.info("↓↓ 【unfreeze frame】");
402
+ break;
403
+ }
404
+ case RECEIVE.LatencyTest: {
405
+ const latencyTimings = JSON.parse(utf16.decode(data.slice(1)));
406
+ console.info("↓↓ latency timings:", latencyTimings);
407
+ break;
408
+ }
409
+ case RECEIVE.QualityControlOwnership: {
410
+ this.QualityControlOwnership = data[1] !== 0;
411
+ console.info("↓↓ Quality Control Ownership:", this.QualityControlOwnership);
412
+ break;
413
+ }
414
+ case RECEIVE.InitialSettings: {
415
+ this.InitialSettings = JSON.parse(utf16.decode(data.slice(1)));
416
+ console.log("↓↓ initial setting:", this.InitialSettings);
417
+ break;
418
+ }
419
+ case RECEIVE.InputControlOwnership: {
420
+ this.InputControlOwnership = data[1] !== 0;
421
+ console.log("↓↓ input control ownership:", this.InputControlOwnership);
422
+ break;
423
+ }
424
+ case RECEIVE.Protocol: {
425
+ let protocol = JSON.parse(utf16.decode(data.slice(1)));
426
+ console.log(protocol)
427
+ if (protocol.Direction === 0) {
428
+ for (let key in protocol) {
429
+ SEND[key] = protocol[key].id
430
+ }
431
+ } else if (protocol.Direction === 1) {
432
+ for (let key in protocol) {
433
+ RECEIVE[key] = protocol[key].id
434
+ }
435
+ }
436
+
437
+ this.dc.send(new Uint8Array([SEND.RequestInitialSettings]));
438
+ this.dc.send(new Uint8Array([SEND.RequestQualityControl]));
439
+
440
+ break
441
+ }
442
+ default: {
443
+ console.error("↓↓ invalid data:", data);
444
+ }
445
+ }
446
+ }
447
+
448
+ setupVideo() {
449
+ this.tabIndex = 0; // easy to focus..
450
+ // this.autofocus = true;
451
+ this.playsInline = true;
452
+ this.disablepictureinpicture = true;
453
+
454
+ // Recently many browsers can only autoplay the videos with sound off
455
+ this.muted = true;
456
+ this.autoplay = true;
457
+
458
+ // this.onsuspend
459
+ // this.onresize
460
+ // this.requestPointerLock();
461
+
462
+ this.style["pointer-events"] = "none";
463
+ this.style["object-fit"] = "fill";
464
+ }
465
+
466
+ setupDataChannel(e) {
467
+ // See https://www.w3.org/TR/webrtc/#dom-rtcdatachannelinit for values (this is needed for Firefox to be consistent with Chrome.)
468
+ // this.dc = this.pc.createDataChannel(label, { ordered: true });
469
+
470
+ this.dc = e.channel;
471
+
472
+ // Inform browser we would like binary data as an ArrayBuffer (FF chooses Blob by default!)
473
+ this.dc.binaryType = "arraybuffer";
474
+
475
+ this.dc.onopen = (e) => {
476
+ console.log("✅", this.dc);
477
+ this.style.pointerEvents = "auto";
478
+
479
+ // setTimeout(() => {
480
+ // this.dc.send(new Uint8Array([SEND.RequestInitialSettings]));
481
+ // this.dc.send(new Uint8Array([SEND.RequestQualityControl]));
482
+ // }, 500);
483
+ };
484
+
485
+
486
+
487
+ this.dc.onclose = (e) => {
488
+ console.info("❌ data channel closed");
489
+ this.style.pointerEvents = "none";
490
+ this.blur();
491
+ };
492
+
493
+ this.dc.onerror;
494
+
495
+ this.dc.onmessage = (e) => {
496
+ this.onDataChannelMessage(e.data);
497
+ };
498
+ }
499
+
500
+ setupDataChannel_ue4(label = 'hello') {
501
+ // See https://www.w3.org/TR/webrtc/#dom-rtcdatachannelinit for values (this is needed for Firefox to be consistent with Chrome.)
502
+ this.dc = this.pc.createDataChannel(label, { ordered: true })
503
+ // Inform browser we would like binary data as an ArrayBuffer (FF chooses Blob by default!)
504
+ this.dc.binaryType = 'arraybuffer'
505
+
506
+ this.dc.onopen = (e) => {
507
+ console.log('✅ data channel connected:', label)
508
+ this.style.pointerEvents = 'auto'
509
+ this.dc.send(new Uint8Array([SEND.RequestInitialSettings]))
510
+ this.dc.send(new Uint8Array([SEND.RequestQualityControl]))
511
+ }
512
+
513
+ this.dc.onclose = (e) => {
514
+ console.info('❌ data channel closed:', label)
515
+ this.style.pointerEvents = 'none'
516
+ }
517
+
518
+ this.dc.onmessage = (e) => {
519
+ this.onDataChannelMessage(e.data)
520
+ }
521
+ }
522
+
523
+ setupPeerConnection() {
524
+ this.pc.close();
525
+ this.pc = new RTCPeerConnection({
526
+ sdpSemantics: "unified-plan",
527
+ bundlePolicy: "balanced",
528
+ iceServers: iceServers
529
+ });
530
+
531
+ this.pc.ontrack = (e) => {
532
+ console.log(`↓↓ ${e.track.kind} track:`, e);
533
+ if (e.track.kind === "video") {
534
+ this.srcObject = e.streams[0];
535
+ } else if (e.track.kind === "audio") {
536
+ this.audio = document.createElement("audio");
537
+ this.audio.autoplay = true;
538
+ this.audio.srcObject = e.streams[0];
539
+ }
540
+ };
541
+ this.pc.onicecandidate = (e) => {
542
+ // firefox
543
+ if (e.candidate?.candidate) {
544
+ console.log("↑↑ candidate:", e.candidate);
545
+ this.ws.send(JSON.stringify({ type: "iceCandidate", candidate: e.candidate }));
546
+ } else {
547
+ // Notice that the end of negotiation is detected here when the event"s candidate property is null.
548
+ }
549
+ };
550
+
551
+ this.pc.ondatachannel = (e) => {
552
+ this.setupDataChannel(e);
553
+ };
554
+ }
555
+
556
+ setupPeerConnection_ue4() {
557
+ this.pc.close()
558
+ this.pc = new RTCPeerConnection({
559
+ sdpSemantics: 'unified-plan',
560
+ bundlePolicy: 'balanced',
561
+ iceServers: iceServers
562
+ })
563
+
564
+ this.pc.ontrack = (e) => {
565
+ console.log(`↓↓ ${e.track.kind} track:`, e)
566
+ if (e.track.kind === 'video') {
567
+ this.srcObject = e.streams[0]
568
+ } else if (e.track.kind === 'audio') {
569
+ this.audio = document.createElement('audio')
570
+ this.audio.autoplay = true
571
+ this.audio.srcObject = e.streams[0]
572
+ }
573
+ }
574
+ this.pc.onicecandidate = (e) => {
575
+ // firefox
576
+ if (e.candidate?.candidate) {
577
+ console.log('↑↑ candidate:', e.candidate)
578
+ this.ws.send(
579
+ JSON.stringify({ type: 'iceCandidate', candidate: e.candidate })
580
+ )
581
+ } else {
582
+ // Notice that the end of negotiation is detected here when the event"s candidate property is null.
583
+ }
584
+ }
585
+ this.pc.onnegotiationneeded = (e) => {
586
+ this.setupOffer()
587
+ }
588
+ }
589
+
590
+ async setupOffer() {
591
+ // this.pc.addTransceiver("video", { direction: "recvonly" });
592
+
593
+ const offer = await this.pc.createOffer({
594
+ offerToReceiveAudio: +this.hasAttribute('audio'),
595
+ offerToReceiveVideo: 1,
596
+ voiceActivityDetection: false,
597
+ })
598
+
599
+ // this indicate we support stereo (Chrome needs this)
600
+ offer.sdp = offer.sdp.replace(
601
+ 'useinbandfec=1',
602
+ 'useinbandfec=1;stereo=1;sprop-maxcapturerate=48000'
603
+ )
604
+
605
+ this.pc.setLocalDescription(offer)
606
+
607
+ this.ws.send(JSON.stringify(offer))
608
+ console.log('↓↓ sending offer:', offer)
609
+ }
610
+
611
+ keysDown = new Set()
612
+
613
+ registerKeyboardEvents() {
614
+ this.onkeydown = (e) => {
615
+ const keyCode = SpecialKeyCodes[e.code] || e.keyCode
616
+ this.dc.send(new Uint8Array([SEND.KeyDown, keyCode, e.repeat]));
617
+ this.keysDown.add(keyCode)
618
+
619
+ // Backspace is not considered a keypress in JavaScript but we need it
620
+ // to be so characters may be deleted in a UE text entry field.
621
+ if (e.keyCode === SpecialKeyCodes.Backspace) {
622
+ this.onkeypress({
623
+ keyCode: SpecialKeyCodes.Backspace
624
+ });
625
+ }
626
+ // whether to prevent browser"s default behavior when keyboard/mouse have inputs, like F1~F12 and Tab
627
+ // e.preventDefault();
628
+ };
629
+
630
+ this.onkeyup = (e) => {
631
+ const keyCode = SpecialKeyCodes[e.code] || e.keyCode
632
+ this.dc.send(new Uint8Array([SEND.KeyUp, keyCode]));
633
+ this.keysDown.delete(keyCode)
634
+ };
635
+
636
+ this.onkeypress = (e) => {
637
+ const data = new DataView(new ArrayBuffer(3));
638
+ data.setUint8(0, SEND.KeyPress);
639
+ data.setUint16(1, SpecialKeyCodes[e.code] || e.keyCode, true);
640
+ this.dc.send(data);
641
+ };
642
+
643
+ this.onblur = e => {
644
+ this.keysDown.forEach(keyCode => {
645
+ this.dc.send(new Uint8Array([SEND.KeyUp, keyCode]));
646
+ })
647
+ this.keysDown.clear()
648
+ }
649
+ }
650
+
651
+ registerTouchEvents() {
652
+ // We need to assign a unique identifier to each finger.
653
+ // We do this by mapping each Touch object to the identifier.
654
+ const fingers = [9, 8, 7, 6, 5, 4, 3, 2, 1, 0];
655
+ const fingerIds = {};
656
+
657
+ this.ontouchstart = (e) => {
658
+ // Assign a unique identifier to each touch.
659
+ for (const touch of e.changedTouches) {
660
+ // remember touch
661
+ const finger = fingers.pop();
662
+ if (finger === undefined) {
663
+ console.info("exhausted touch indentifiers");
664
+ }
665
+ fingerIds[touch.identifier] = finger;
666
+ }
667
+ this.emitTouchData(SEND.TouchStart, e.changedTouches, fingerIds);
668
+ e.preventDefault();
669
+ };
670
+
671
+ this.ontouchend = (e) => {
672
+ this.emitTouchData(SEND.TouchEnd, e.changedTouches, fingerIds);
673
+ // Re-cycle unique identifiers previously assigned to each touch.
674
+ for (const touch of e.changedTouches) {
675
+ // forget touch
676
+ fingers.push(fingerIds[touch.identifier]);
677
+ delete fingerIds[touch.identifier];
678
+ }
679
+ e.preventDefault();
680
+ };
681
+
682
+ this.ontouchmove = (e) => {
683
+ this.emitTouchData(SEND.TouchMove, e.touches, fingerIds);
684
+ e.preventDefault();
685
+ };
686
+ }
687
+
688
+ // touch as mouse
689
+ registerFakeMouseEvents() {
690
+ let finger = undefined;
691
+
692
+ const { left, top } = this.getBoundingClientRect();
693
+
694
+ this.ontouchstart = (e) => {
695
+ if (finger === undefined) {
696
+ const firstTouch = e.changedTouches[0];
697
+ finger = {
698
+ id: firstTouch.identifier,
699
+ x: firstTouch.clientX - left,
700
+ y: firstTouch.clientY - top,
701
+ };
702
+ // Hack: Mouse events require an enter and leave so we just enter and leave manually with each touch as this event is not fired with a touch device.
703
+ this.onmouseenter(e);
704
+ this.emitMouseDown(MouseButton.MainButton, finger.x, finger.y);
705
+ }
706
+ e.preventDefault();
707
+ };
708
+
709
+ this.ontouchend = (e) => {
710
+ // filtering multi finger touch events temporarily
711
+ if (finger) {
712
+ for (const touch of e.changedTouches) {
713
+ if (touch.identifier === finger.id) {
714
+ const x = touch.clientX - left;
715
+ const y = touch.clientY - top;
716
+ this.emitMouseUp(MouseButton.MainButton, x, y);
717
+ // Hack: Manual mouse leave event.
718
+ this.onmouseleave(e);
719
+ finger = undefined;
720
+ break;
721
+ }
722
+ }
723
+ }
724
+ e.preventDefault();
725
+ };
726
+
727
+ this.ontouchmove = (e) => {
728
+ // filtering multi finger touch events temporarily
729
+ if (finger) {
730
+ for (const touch of e.touches) {
731
+ if (touch.identifier === finger.id) {
732
+ const x = touch.clientX - left;
733
+ const y = touch.clientY - top;
734
+ this.emitMouseMove(x, y, x - finger.x, y - finger.y);
735
+ finger.x = x;
736
+ finger.y = y;
737
+ break;
738
+ }
739
+ }
740
+ }
741
+ e.preventDefault();
742
+ };
743
+ }
744
+
745
+ registerMouseHoverEvents() {
746
+ this.registerMouseEnterAndLeaveEvents();
747
+
748
+ this.onmousemove = (e) => {
749
+ this.emitMouseMove(e.offsetX, e.offsetY, e.movementX, e.movementY);
750
+ e.preventDefault();
751
+ };
752
+
753
+ this.onmousedown = (e) => {
754
+ this.emitMouseDown(e.button, e.offsetX, e.offsetY);
755
+ // e.preventDefault();
756
+ };
757
+
758
+ this.onmouseup = (e) => {
759
+ this.emitMouseUp(e.button, e.offsetX, e.offsetY);
760
+ // e.preventDefault();
761
+ };
762
+
763
+ // When the context menu is shown then it is safest to release the button which was pressed when the event happened. This will guarantee we will get at least one mouse up corresponding to a mouse down event. Otherwise the mouse can get stuck.
764
+ // https://github.com/facebook/react/issues/5531
765
+ this.oncontextmenu = (e) => {
766
+ this.emitMouseUp(e.button, e.offsetX, e.offsetY);
767
+ e.preventDefault();
768
+ };
769
+
770
+ this.onwheel = (e) => {
771
+ this.emitMouseWheel(e.wheelDelta, e.offsetX, e.offsetY);
772
+ e.preventDefault();
773
+ };
774
+ }
775
+
776
+ registerPointerLockEvents() {
777
+ this.registerMouseEnterAndLeaveEvents();
778
+
779
+ console.info("mouse locked in, ESC to exit");
780
+
781
+ const { clientWidth, clientHeight } = this;
782
+ let x = clientWidth / 2;
783
+ let y = clientHeight / 2;
784
+
785
+ this.onmousemove = (e) => {
786
+ x += e.movementX;
787
+ y += e.movementY;
788
+ x = (x + clientWidth) % clientWidth;
789
+ y = (y + clientHeight) % clientHeight;
790
+
791
+ this.emitMouseMove(x, y, e.movementX, e.movementY);
792
+ };
793
+
794
+ this.onmousedown = (e) => {
795
+ this.emitMouseDown(e.button, x, y);
796
+ };
797
+
798
+ this.onmouseup = (e) => {
799
+ this.emitMouseUp(e.button, x, y);
800
+ };
801
+
802
+ this.onwheel = (e) => {
803
+ this.emitMouseWheel(e.wheelDelta, x, y);
804
+ };
805
+ }
806
+
807
+ registerMouseEnterAndLeaveEvents() {
808
+ this.onmouseenter = (e) => {
809
+ this.dc.send(new Uint8Array([SEND.MouseEnter]));
810
+ };
811
+
812
+ this.onmouseleave = (e) => {
813
+ if (this.dc.readyState === "open") this.dc.send(new Uint8Array([SEND.MouseLeave]));
814
+ // 释放掉
815
+ for (let i = 1; i <= 16; i *= 2) {
816
+ if (e.buttons & i) {
817
+ this.emitMouseUp(MouseButtonsMask[i], 0, 0)
818
+ }
819
+ }
820
+ };
821
+ }
822
+
823
+ emitMouseMove(x, y, deltaX, deltaY) {
824
+ const coord = this.normalize(x, y);
825
+ deltaX = (deltaX * 65536) / this.clientWidth;
826
+ deltaY = (deltaY * 65536) / this.clientHeight;
827
+ const data = new DataView(new ArrayBuffer(9));
828
+ data.setUint8(0, SEND.MouseMove);
829
+ data.setUint16(1, coord.x, true);
830
+ data.setUint16(3, coord.y, true);
831
+ data.setInt16(5, deltaX, true);
832
+ data.setInt16(7, deltaY, true);
833
+ this.dc.send(data);
834
+ this.lastmouseTime = new Date()
835
+ }
836
+
837
+ emitMouseDown(button, x, y) {
838
+ const coord = this.normalize(x, y);
839
+ const data = new DataView(new ArrayBuffer(6));
840
+ data.setUint8(0, SEND.MouseDown);
841
+ data.setUint8(1, button);
842
+ data.setUint16(2, coord.x, true);
843
+ data.setUint16(4, coord.y, true);
844
+ this.dc.send(data);
845
+ if (this.enableChinese) {
846
+ this.dc.send(new Uint8Array([SEND.FindFocus]))
847
+ }
848
+ }
849
+
850
+ emitMouseUp(button, x, y) {
851
+ const coord = this.normalize(x, y);
852
+ const data = new DataView(new ArrayBuffer(6));
853
+ data.setUint8(0, SEND.MouseUp);
854
+ data.setUint8(1, button);
855
+ data.setUint16(2, coord.x, true);
856
+ data.setUint16(4, coord.y, true);
857
+ this.dc.send(data);
858
+ }
859
+
860
+ emitMouseWheel(delta, x, y) {
861
+ const coord = this.normalize(x, y);
862
+ const data = new DataView(new ArrayBuffer(7));
863
+ data.setUint8(0, SEND.MouseWheel);
864
+ data.setInt16(1, delta, true);
865
+ data.setUint16(3, coord.x, true);
866
+ data.setUint16(5, coord.y, true);
867
+ this.dc.send(data);
868
+ }
869
+
870
+ emitTouchData(type, touches, fingerIds) {
871
+ const data = new DataView(new ArrayBuffer(2 + 6 * touches.length));
872
+ data.setUint8(0, type);
873
+ data.setUint8(1, touches.length);
874
+ let byte = 2;
875
+ for (const touch of touches) {
876
+ const x = touch.clientX - this.offsetLeft;
877
+ const y = touch.clientY - this.offsetTop;
878
+
879
+ const coord = this.normalize(x, y);
880
+ data.setUint16(byte, coord.x, true);
881
+ byte += 2;
882
+ data.setUint16(byte, coord.y, true);
883
+ byte += 2;
884
+ data.setUint8(byte, fingerIds[touch.identifier], true);
885
+ byte += 1;
886
+ data.setUint8(byte, 255 * touch.force, true); // force is between 0.0 and 1.0 so quantize into byte.
887
+ byte += 1;
888
+ }
889
+ this.dc.send(data);
890
+ }
891
+
892
+ // emit string
893
+ emitMessage(msg, messageType = SEND.UIInteraction) {
894
+ if (typeof msg !== "string") msg = JSON.stringify(msg);
895
+
896
+ // Add the UTF-16 JSON string to the array byte buffer, going two bytes at a time.
897
+ const data = new DataView(new ArrayBuffer(1 + 2 + 2 * msg.length));
898
+ let byteIdx = 0;
899
+ data.setUint8(byteIdx, messageType);
900
+ byteIdx++;
901
+ data.setUint16(byteIdx, msg.length, true);
902
+ byteIdx += 2;
903
+ for (let i = 0; i < msg.length; i++) {
904
+ // charCodeAt() is UTF-16, codePointAt() is Unicode.
905
+ data.setUint16(byteIdx, msg.charCodeAt(i), true);
906
+ byteIdx += 2;
907
+ }
908
+ this.dc.send(data);
909
+
910
+ return new Promise(resolve => this.addEventListener(
911
+ 'message',
912
+ e => resolve(e.detail),
913
+ { once: true }
914
+ ));
915
+ }
916
+
917
+ normalize(x, y) {
918
+ const normalizedX = x / this.clientWidth;
919
+ const normalizedY = y / this.clientHeight;
920
+ if (normalizedX < 0.0 || normalizedX > 1.0 || normalizedY < 0.0 || normalizedY > 1.0) {
921
+ return {
922
+ inRange: false,
923
+ x: 65535,
924
+ y: 65535,
925
+ };
926
+ } else {
927
+ return {
928
+ inRange: true,
929
+ x: normalizedX * 65536,
930
+ y: normalizedY * 65536,
931
+ };
932
+ }
933
+ }
934
+
935
+
936
+ }
937
+
938
+ customElements.define("peer-stream", PeerStream, { extends: "video" });