warpr 0.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 +24 -0
- package/ng-package.json +7 -0
- package/package.json +12 -0
- package/src/lib/components/stream-host/stream-host.component.html +5 -0
- package/src/lib/components/stream-host/stream-host.component.scss +31 -0
- package/src/lib/components/stream-host/stream-host.component.spec.ts +23 -0
- package/src/lib/components/stream-host/stream-host.component.ts +170 -0
- package/src/lib/data/frames.ts +37 -0
- package/src/lib/data/signaling-messages.ts +37 -0
- package/src/lib/data/streaming-messages.ts +136 -0
- package/src/lib/infrastructure/array-stream.ts +109 -0
- package/src/lib/infrastructure/events.ts +61 -0
- package/src/lib/infrastructure/message-assembler.ts +60 -0
- package/src/lib/networking/messaging-client.ts +7 -0
- package/src/lib/networking/web-socket-client.ts +50 -0
- package/src/lib/services/messaging.service.spec.ts +16 -0
- package/src/lib/services/messaging.service.ts +31 -0
- package/src/lib/services/streaming.service.spec.ts +16 -0
- package/src/lib/services/streaming.service.ts +150 -0
- package/src/public-api.ts +15 -0
- package/tsconfig.lib.json +14 -0
- package/tsconfig.lib.prod.json +10 -0
- package/tsconfig.spec.json +14 -0
package/README.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# WarprLib
|
|
2
|
+
|
|
3
|
+
This library was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.3.0.
|
|
4
|
+
|
|
5
|
+
## Code scaffolding
|
|
6
|
+
|
|
7
|
+
Run `ng generate component component-name --project warpr-lib` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module --project warpr-lib`.
|
|
8
|
+
> Note: Don't forget to add `--project warpr-lib` or else it will be added to the default project in your `angular.json` file.
|
|
9
|
+
|
|
10
|
+
## Build
|
|
11
|
+
|
|
12
|
+
Run `ng build warpr-lib` to build the project. The build artifacts will be stored in the `dist/` directory.
|
|
13
|
+
|
|
14
|
+
## Publishing
|
|
15
|
+
|
|
16
|
+
After building your library with `ng build warpr-lib`, go to the dist folder `cd dist/warpr-lib` and run `npm publish`.
|
|
17
|
+
|
|
18
|
+
## Running unit tests
|
|
19
|
+
|
|
20
|
+
Run `ng test warpr-lib` to execute the unit tests via [Karma](https://karma-runner.github.io).
|
|
21
|
+
|
|
22
|
+
## Further help
|
|
23
|
+
|
|
24
|
+
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.
|
package/ng-package.json
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
.root {
|
|
2
|
+
background-color: black;
|
|
3
|
+
color: white;
|
|
4
|
+
display: flex;
|
|
5
|
+
position: relative;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.renderTarget {
|
|
9
|
+
width: 100%;
|
|
10
|
+
height: 100%;
|
|
11
|
+
object-fit: contain;
|
|
12
|
+
touch-action: none;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.overlay {
|
|
16
|
+
position: absolute;
|
|
17
|
+
bottom: 10px;
|
|
18
|
+
right: 10px;
|
|
19
|
+
background-color: rgba(0, 0, 0, 0.5);
|
|
20
|
+
color: rgba(255,255,255,0.75);
|
|
21
|
+
padding: 10px;
|
|
22
|
+
border-radius: 5px;
|
|
23
|
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
24
|
+
font-weight: bold;
|
|
25
|
+
cursor: pointer;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.overlay:hover {
|
|
29
|
+
background-color: rgba(0, 0, 0, 0.75);
|
|
30
|
+
color: white;
|
|
31
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
2
|
+
|
|
3
|
+
import { StreamHostComponent } from './stream-host.component';
|
|
4
|
+
|
|
5
|
+
describe('StreamHostComponent', () => {
|
|
6
|
+
let component: StreamHostComponent;
|
|
7
|
+
let fixture: ComponentFixture<StreamHostComponent>;
|
|
8
|
+
|
|
9
|
+
beforeEach(async () => {
|
|
10
|
+
await TestBed.configureTestingModule({
|
|
11
|
+
declarations: [StreamHostComponent]
|
|
12
|
+
})
|
|
13
|
+
.compileComponents();
|
|
14
|
+
|
|
15
|
+
fixture = TestBed.createComponent(StreamHostComponent);
|
|
16
|
+
component = fixture.componentInstance;
|
|
17
|
+
fixture.detectChanges();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should create', () => {
|
|
21
|
+
expect(component).toBeTruthy();
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { Component, ElementRef, ViewChild } from '@angular/core';
|
|
2
|
+
import { StreamingService } from '../../services/streaming.service';
|
|
3
|
+
import { EncodedFrame, FrameType, Size } from '../../data/frames';
|
|
4
|
+
import { PointerInputMessage, PointerStates, ResizeSurfaceMessage } from '../../data/streaming-messages';
|
|
5
|
+
import { CommonModule } from '@angular/common';
|
|
6
|
+
|
|
7
|
+
@Component({
|
|
8
|
+
selector: 'warpr-stream-host',
|
|
9
|
+
standalone: true,
|
|
10
|
+
imports: [CommonModule],
|
|
11
|
+
templateUrl: './stream-host.component.html',
|
|
12
|
+
styleUrl: './stream-host.component.scss'
|
|
13
|
+
})
|
|
14
|
+
export class StreamHostComponent {
|
|
15
|
+
|
|
16
|
+
@ViewChild("root")
|
|
17
|
+
private _root?: ElementRef<HTMLDivElement>;
|
|
18
|
+
|
|
19
|
+
@ViewChild("renderTarget")
|
|
20
|
+
private _canvas?: ElementRef<HTMLCanvasElement>;
|
|
21
|
+
private _renderingContext: CanvasRenderingContext2D | null = null;
|
|
22
|
+
|
|
23
|
+
private _decoder: VideoDecoder;
|
|
24
|
+
private _framePending?: VideoFrame;
|
|
25
|
+
private _videoSize = Size.Empty;
|
|
26
|
+
private _displaySize = Size.Empty;
|
|
27
|
+
private _isDecoderInitialized = false;
|
|
28
|
+
private _pointerStates: PointerStates = {};
|
|
29
|
+
|
|
30
|
+
public get IsFullScreenAvailable(): boolean {
|
|
31
|
+
return document.fullscreenEnabled;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public constructor(
|
|
35
|
+
private _streamingService: StreamingService) {
|
|
36
|
+
this._streamingService.Connected.Subscribe((sender, eventArgs) => this.OnConnected());
|
|
37
|
+
this._streamingService.FrameReceived.Subscribe((sender, eventArgs) => this.OnFrameReceived(eventArgs));
|
|
38
|
+
|
|
39
|
+
this._decoder = new VideoDecoder({
|
|
40
|
+
output: (frame) => this.OnFrameDecoded(frame),
|
|
41
|
+
error: (error) => console.log(error)
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private async ngAfterViewInit() {
|
|
46
|
+
|
|
47
|
+
console.log("Initializing stream host...");
|
|
48
|
+
|
|
49
|
+
let canvas = this._canvas!.nativeElement;
|
|
50
|
+
this._renderingContext = canvas.getContext("2d")!;
|
|
51
|
+
canvas.onpointerdown = (event) => this.OnPointerEvent(event);
|
|
52
|
+
canvas.onpointermove = (event) => this.OnPointerEvent(event);
|
|
53
|
+
canvas.onpointerup = (event) => this.OnPointerEvent(event);
|
|
54
|
+
canvas.onwheel = (event) => this.OnWheelEvent(event);
|
|
55
|
+
canvas.oncontextmenu = (event) => event.preventDefault();
|
|
56
|
+
|
|
57
|
+
if (this._renderingContext) {
|
|
58
|
+
console.log("Stream host initialized.");
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
console.log("Stream host failed to initialize.");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
requestAnimationFrame(() => this.RenderFrame());
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private OnPointerEvent(event: PointerEvent, wheelDelta = 0) {
|
|
68
|
+
//console.log(event);
|
|
69
|
+
event.preventDefault();
|
|
70
|
+
|
|
71
|
+
if (event.type == "pointerdown") this._canvas!.nativeElement.setPointerCapture(event.pointerId);
|
|
72
|
+
|
|
73
|
+
let message = new PointerInputMessage(event, this._pointerStates, this._canvas!.nativeElement, wheelDelta);
|
|
74
|
+
//console.log(message);
|
|
75
|
+
|
|
76
|
+
this._streamingService.SendMessage(message);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private OnWheelEvent(event: WheelEvent) {
|
|
80
|
+
//console.log(event);
|
|
81
|
+
event.preventDefault();
|
|
82
|
+
|
|
83
|
+
let pointerEvent: PointerEvent = new PointerEvent(event.type, event);
|
|
84
|
+
this.OnPointerEvent(pointerEvent, event.deltaY);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private OnConnected() {
|
|
88
|
+
this._displaySize = Size.Empty;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private OnFrameReceived(frame: EncodedFrame) {
|
|
92
|
+
let chunk = new EncodedVideoChunk({
|
|
93
|
+
data: frame.Bytes,
|
|
94
|
+
type: frame.Type == FrameType.Key ? "key" : "delta",
|
|
95
|
+
timestamp: frame.Index
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
//Initialize decoder
|
|
99
|
+
let frameSize = new Size(frame.Width, frame.Height);
|
|
100
|
+
|
|
101
|
+
if (!Size.AreEqual(frameSize, this._videoSize)) {
|
|
102
|
+
console.log("Configuring decoder...");
|
|
103
|
+
this._decoder.configure({
|
|
104
|
+
codec: "hvc1.1.6.L93.B0",
|
|
105
|
+
codedWidth: frame.Width,
|
|
106
|
+
codedHeight: frame.Height,
|
|
107
|
+
hardwareAcceleration: "prefer-hardware",
|
|
108
|
+
optimizeForLatency: true
|
|
109
|
+
});
|
|
110
|
+
console.log("Decoder configured.");
|
|
111
|
+
console.log(`Resolution is ${frame.Width} x ${frame.Height}.`);
|
|
112
|
+
|
|
113
|
+
this._videoSize = frameSize;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
//Decode frame
|
|
117
|
+
if (!this._isDecoderInitialized && chunk.type != "key") return;
|
|
118
|
+
this._isDecoderInitialized = true;
|
|
119
|
+
|
|
120
|
+
this._decoder.decode(chunk);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private OnFrameDecoded(frame: VideoFrame) {
|
|
124
|
+
if (this._framePending) this._framePending.close();
|
|
125
|
+
this._framePending = frame;
|
|
126
|
+
requestAnimationFrame(() => this.RenderFrame());
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private RenderFrame() {
|
|
130
|
+
|
|
131
|
+
let frame = this._framePending;
|
|
132
|
+
this._framePending = undefined;
|
|
133
|
+
|
|
134
|
+
if (frame) {
|
|
135
|
+
|
|
136
|
+
//Set canvas resolution to video resolution
|
|
137
|
+
let canvas = this._canvas!.nativeElement;
|
|
138
|
+
canvas.width = frame.displayWidth;
|
|
139
|
+
canvas.height = frame.displayHeight;
|
|
140
|
+
|
|
141
|
+
//Check for control size change
|
|
142
|
+
let canvasRect = canvas.getBoundingClientRect();
|
|
143
|
+
let canvasSize = new Size(Math.round(canvasRect.width * window.devicePixelRatio), Math.round(canvasRect.height * window.devicePixelRatio));
|
|
144
|
+
if (!Size.AreEqual(canvasSize, this._displaySize)) {
|
|
145
|
+
this._displaySize = canvasSize;
|
|
146
|
+
|
|
147
|
+
console.log(`Resized to ${canvasSize.Width} x ${canvasSize.Height}`);
|
|
148
|
+
let message = new ResizeSurfaceMessage(canvasSize.Width, canvasSize.Height);
|
|
149
|
+
this._streamingService.SendMessage(message);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
//Draw new frame
|
|
153
|
+
if (this._renderingContext) {
|
|
154
|
+
this._renderingContext.drawImage(frame, 0, 0, frame.displayWidth, frame.displayHeight);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
//Release memory
|
|
158
|
+
frame.close();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
public ToggleFullScreen() {
|
|
163
|
+
if (document.fullscreenElement === this._root?.nativeElement) {
|
|
164
|
+
document.exitFullscreen();
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
this._root?.nativeElement.requestFullscreen();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { ArrayStream } from "../infrastructure/array-stream";
|
|
2
|
+
|
|
3
|
+
export enum FrameType {
|
|
4
|
+
Unknown,
|
|
5
|
+
Key,
|
|
6
|
+
Delta
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class EncodedFrame {
|
|
10
|
+
public Index: number;
|
|
11
|
+
public Type: FrameType;
|
|
12
|
+
public Width: number;
|
|
13
|
+
public Height: number;
|
|
14
|
+
public Bytes: ArrayBuffer;
|
|
15
|
+
|
|
16
|
+
public constructor(buffer: ArrayBuffer) {
|
|
17
|
+
let stream = new ArrayStream(buffer);
|
|
18
|
+
|
|
19
|
+
this.Type = stream.ReadUInt32();
|
|
20
|
+
this.Index = stream.ReadUInt32();
|
|
21
|
+
this.Width = stream.ReadUInt32();
|
|
22
|
+
this.Height = stream.ReadUInt32();
|
|
23
|
+
this.Bytes = stream.ReadBuffer();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class Size {
|
|
28
|
+
public constructor(
|
|
29
|
+
public Width: number,
|
|
30
|
+
public Height: number) { }
|
|
31
|
+
|
|
32
|
+
public static Empty: Size = { Width: 0, Height: 0 };
|
|
33
|
+
|
|
34
|
+
public static AreEqual(a: Size, b: Size) {
|
|
35
|
+
return a.Width === b.Width && a.Height === b.Height;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
|
|
2
|
+
export enum WarprSignalingMessageType {
|
|
3
|
+
ConnectionRequest = "ConnectionRequest",
|
|
4
|
+
PairingCompleteMessage = "PairingCompleteMessage",
|
|
5
|
+
PeerConnectionDescriptionMessage = "PeerConnectionDescriptionMessage",
|
|
6
|
+
PeerConnectionCandidateMessage = "PeerConnectionCandidateMessage",
|
|
7
|
+
StreamingSourcesMessage = "StreamingSourcesMessage",
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class ConnectionRequest {
|
|
11
|
+
public readonly $type = WarprSignalingMessageType.ConnectionRequest;
|
|
12
|
+
public SessionId?: string;
|
|
13
|
+
}
|
|
14
|
+
export class PairingCompleteMessage {
|
|
15
|
+
public readonly $type = WarprSignalingMessageType.PairingCompleteMessage;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class PeerConnectionDescriptionMessage {
|
|
19
|
+
public readonly $type = WarprSignalingMessageType.PeerConnectionDescriptionMessage;
|
|
20
|
+
public Description?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class PeerConnectionCandidateMessage {
|
|
24
|
+
public readonly $type = WarprSignalingMessageType.PeerConnectionCandidateMessage;
|
|
25
|
+
public Candidate?: string;
|
|
26
|
+
}
|
|
27
|
+
export class StreamingSourcesMessage {
|
|
28
|
+
public readonly $type = WarprSignalingMessageType.StreamingSourcesMessage;
|
|
29
|
+
public Sources?: string[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type WarprSignalingMessage =
|
|
33
|
+
ConnectionRequest |
|
|
34
|
+
PairingCompleteMessage |
|
|
35
|
+
PeerConnectionDescriptionMessage |
|
|
36
|
+
PeerConnectionCandidateMessage |
|
|
37
|
+
StreamingSourcesMessage;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
export enum WarprStreamingMessageType {
|
|
2
|
+
PointerInputMessage = "PointerInputMessage",
|
|
3
|
+
ResizeSurfaceMessage = "ResizeSurfaceMessage"
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export type Point = {
|
|
7
|
+
X: number;
|
|
8
|
+
Y: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export enum PointerType {
|
|
12
|
+
Unknown = "Unknown",
|
|
13
|
+
Mouse = "Mouse",
|
|
14
|
+
Pen = "Pen",
|
|
15
|
+
Touch = "Touch"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export enum PointerAction {
|
|
19
|
+
Unknown = "Unknown",
|
|
20
|
+
Pressed = "Pressed",
|
|
21
|
+
Moved = "Moved",
|
|
22
|
+
Released = "Released",
|
|
23
|
+
WheelChanged = "WheelChanged"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export enum PointerFlags {
|
|
27
|
+
None = 0,
|
|
28
|
+
LeftButton = 1,
|
|
29
|
+
MiddleButton = 2,
|
|
30
|
+
RightButton = 4,
|
|
31
|
+
Wheel = 8,
|
|
32
|
+
Captured = 16
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export class PointerState {
|
|
36
|
+
public Buttons = 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type PointerStates = { [key: number]: PointerState };
|
|
40
|
+
|
|
41
|
+
export class PointerInputMessage {
|
|
42
|
+
public readonly $type = WarprStreamingMessageType.PointerInputMessage;
|
|
43
|
+
|
|
44
|
+
public Id = -1;
|
|
45
|
+
public Type = PointerType.Unknown;
|
|
46
|
+
public Action = PointerAction.Unknown;
|
|
47
|
+
public Position: Point = { X: 0, Y: 0 };
|
|
48
|
+
public Flags = PointerFlags.None;
|
|
49
|
+
public WheelDelta = 0;
|
|
50
|
+
|
|
51
|
+
constructor(event: PointerEvent, states: PointerStates, target: HTMLCanvasElement, wheelDelta: number) {
|
|
52
|
+
|
|
53
|
+
let state = states[event.pointerId];
|
|
54
|
+
if (state === undefined) {
|
|
55
|
+
state = new PointerState();
|
|
56
|
+
states[event.pointerId] = state;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
this.Id = event.pointerId;
|
|
60
|
+
|
|
61
|
+
switch (event.pointerType) {
|
|
62
|
+
case "mouse":
|
|
63
|
+
this.Type = PointerType.Mouse;
|
|
64
|
+
break;
|
|
65
|
+
case "pen":
|
|
66
|
+
this.Type = PointerType.Pen;
|
|
67
|
+
break;
|
|
68
|
+
case "touch":
|
|
69
|
+
this.Type = PointerType.Touch;
|
|
70
|
+
break;
|
|
71
|
+
default:
|
|
72
|
+
this.Type = PointerType.Mouse;
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
switch (event.type) {
|
|
77
|
+
case "pointerdown":
|
|
78
|
+
this.Action = PointerAction.Pressed;
|
|
79
|
+
state.Buttons |= 1 << event.button
|
|
80
|
+
break;
|
|
81
|
+
case "pointerup":
|
|
82
|
+
this.Action = PointerAction.Released;
|
|
83
|
+
state.Buttons &= ~(1 << event.button)
|
|
84
|
+
break;
|
|
85
|
+
case "pointermove":
|
|
86
|
+
this.Action = PointerAction.Moved;
|
|
87
|
+
break;
|
|
88
|
+
case "wheel":
|
|
89
|
+
this.Action = PointerAction.WheelChanged;
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (state.Buttons & 1) this.Flags |= PointerFlags.LeftButton;
|
|
94
|
+
if (state.Buttons & 2) this.Flags |= PointerFlags.MiddleButton;
|
|
95
|
+
if (state.Buttons & 4) this.Flags |= PointerFlags.RightButton;
|
|
96
|
+
if (wheelDelta != 0) this.Flags |= PointerFlags.Wheel;
|
|
97
|
+
if (target.hasPointerCapture(event.pointerId)) this.Flags |= PointerFlags.Captured;
|
|
98
|
+
|
|
99
|
+
this.Position = PointerInputMessage.GetRemotePosition(event, target);
|
|
100
|
+
this.WheelDelta = -wheelDelta;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private static GetRemotePosition(event: PointerEvent, target: HTMLCanvasElement): Point {
|
|
104
|
+
let offsetX = 0;
|
|
105
|
+
let offsetY = 0;
|
|
106
|
+
let scale = 1;
|
|
107
|
+
let canvasAspectRatio = target.clientWidth / target.clientHeight;
|
|
108
|
+
let videoAspectRatio = target.width / target.height;
|
|
109
|
+
|
|
110
|
+
if (videoAspectRatio > canvasAspectRatio) {
|
|
111
|
+
offsetY = 0.5 * (target.clientHeight - target.clientWidth / videoAspectRatio);
|
|
112
|
+
scale = target.width / target.clientWidth;
|
|
113
|
+
} else {
|
|
114
|
+
offsetX = 0.5 * (target.clientWidth - target.clientHeight * videoAspectRatio);
|
|
115
|
+
scale = target.height / target.clientHeight;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
X: (event.x - offsetX) * scale,
|
|
120
|
+
Y: (event.y - offsetY) * scale
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export class ResizeSurfaceMessage {
|
|
126
|
+
public readonly $type = WarprStreamingMessageType.ResizeSurfaceMessage;
|
|
127
|
+
|
|
128
|
+
constructor(
|
|
129
|
+
public Width: number,
|
|
130
|
+
public Height: number
|
|
131
|
+
) { }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export type WarprStreamingMessage =
|
|
135
|
+
PointerInputMessage |
|
|
136
|
+
ResizeSurfaceMessage;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
export class ArrayStream {
|
|
2
|
+
private _view: DataView;
|
|
3
|
+
private _position: number;
|
|
4
|
+
|
|
5
|
+
public constructor(buffer: ArrayBuffer) {
|
|
6
|
+
this._view = new DataView(buffer);
|
|
7
|
+
this._position = 0;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
public get Position(): number {
|
|
11
|
+
return this._position;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
public set Position(value: number) {
|
|
15
|
+
if (value < 0 || this._view.buffer.byteLength < value) {
|
|
16
|
+
throw new RangeError("The provided value lies outside the stream's bounds.");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
this._position = value;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public ReadBool(): boolean {
|
|
23
|
+
let result = this._view.getUint8(this._position) != 0;
|
|
24
|
+
this._position += 1;
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public ReadUInt8(): number {
|
|
29
|
+
let result = this._view.getUint8(this._position);
|
|
30
|
+
this._position += 1;
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public ReadUInt16(): number {
|
|
35
|
+
let result = this._view.getUint16(this._position, true);
|
|
36
|
+
this._position += 2;
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public ReadUInt32(): number {
|
|
41
|
+
let result = this._view.getUint32(this._position, true);
|
|
42
|
+
this._position += 4;
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public ReadUInt64(): bigint {
|
|
47
|
+
let result = this._view.getBigUint64(this._position, true);
|
|
48
|
+
this._position += 8;
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public ReadInt8(): number {
|
|
53
|
+
let result = this._view.getInt8(this._position);
|
|
54
|
+
this._position += 1;
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public ReadInt16(): number {
|
|
59
|
+
let result = this._view.getInt16(this._position, true);
|
|
60
|
+
this._position += 2;
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public ReadInt32(): number {
|
|
65
|
+
let result = this._view.getInt32(this._position, true);
|
|
66
|
+
this._position += 4;
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public ReadInt64(): bigint {
|
|
71
|
+
let result = this._view.getBigInt64(this._position, true);
|
|
72
|
+
this._position += 8;
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
public ReadSingle(): number {
|
|
77
|
+
let result = this._view.getFloat32(this._position, true);
|
|
78
|
+
this._position += 4;
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
public ReadDouble(): number {
|
|
83
|
+
let result = this._view.getFloat64(this._position, true);
|
|
84
|
+
this._position += 8;
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
public ReadBuffer(): ArrayBuffer {
|
|
89
|
+
let length = this.ReadUInt32();
|
|
90
|
+
let result = this._view.buffer.slice(this._position, this._position + length);
|
|
91
|
+
this._position += length;
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
public ReadToEnd(): ArrayBuffer {
|
|
96
|
+
let length = this._view.buffer.byteLength - this._position;
|
|
97
|
+
let result = this._view.buffer.slice(this._position, this._position + length);
|
|
98
|
+
this._position += length;
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
public ReadString() {
|
|
103
|
+
let length = this.ReadUInt32();
|
|
104
|
+
let decoder = new TextDecoder();
|
|
105
|
+
let result = decoder.decode(this._view.buffer.slice(this._position, this._position + length));
|
|
106
|
+
this._position += length;
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export type EventHandler<TSender, TEventArgs> = (sender: TSender, eventArgs: TEventArgs) => void;
|
|
2
|
+
|
|
3
|
+
export interface IEventSource {
|
|
4
|
+
|
|
5
|
+
Unsubscribe(token: symbol): void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class EventSubscription {
|
|
9
|
+
|
|
10
|
+
constructor(
|
|
11
|
+
private _owner: IEventSource,
|
|
12
|
+
private _token: symbol) { }
|
|
13
|
+
|
|
14
|
+
Unsubscribe() {
|
|
15
|
+
this._owner.Unsubscribe(this._token);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class EventOwner {
|
|
20
|
+
|
|
21
|
+
public readonly Token = Symbol();
|
|
22
|
+
|
|
23
|
+
Raise<TSender, TEventArgs>(event: EventPublisher<TSender, TEventArgs>, sender: TSender, eventArgs: TEventArgs) {
|
|
24
|
+
event.Raise(this, sender, eventArgs);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class EventArgs {
|
|
29
|
+
public static readonly Empty = new EventArgs();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class EventPublisher<TSender, TEventArgs> implements IEventSource {
|
|
33
|
+
|
|
34
|
+
private readonly _token: symbol;
|
|
35
|
+
private readonly _handlers = new Map<symbol, EventHandler<TSender, TEventArgs>>;
|
|
36
|
+
|
|
37
|
+
constructor(owner: EventOwner) {
|
|
38
|
+
this._token = owner.Token;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
Subscribe(handler: EventHandler<TSender, TEventArgs>) {
|
|
42
|
+
const token = Symbol();
|
|
43
|
+
this._handlers.set(token, handler);
|
|
44
|
+
return new EventSubscription(this, token);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
Raise(owner: EventOwner, sender: TSender, eventArgs: TEventArgs): void {
|
|
48
|
+
if (owner.Token === this._token) {
|
|
49
|
+
for (let [symbol, handler] of this._handlers) {
|
|
50
|
+
handler(sender, eventArgs);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
throw Error("Cannot raise event: this event source does not belong to the specified owner.");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
Unsubscribe(token: symbol): void {
|
|
59
|
+
this._handlers.delete(token);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { ArrayStream } from "./array-stream";
|
|
2
|
+
|
|
3
|
+
class MessageBuilder {
|
|
4
|
+
public readonly FragmentCount: number;
|
|
5
|
+
public FragmentsReady: number = 0;
|
|
6
|
+
public Buffer: Uint8Array;
|
|
7
|
+
|
|
8
|
+
constructor(
|
|
9
|
+
public readonly Id: number,
|
|
10
|
+
public readonly Size: number,
|
|
11
|
+
public readonly FragmentSize: number
|
|
12
|
+
) {
|
|
13
|
+
this.FragmentCount = Math.ceil(Size / FragmentSize);
|
|
14
|
+
this.Buffer = new Uint8Array(Size);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
AddFragment(index: number, buffer: Uint8Array): boolean {
|
|
18
|
+
if (this.FragmentCount == this.FragmentsReady) throw RangeError("All message parts have been already collected.");
|
|
19
|
+
|
|
20
|
+
this.Buffer.set(buffer, this.FragmentSize * index);
|
|
21
|
+
this.FragmentsReady++;
|
|
22
|
+
|
|
23
|
+
return this.FragmentCount == this.FragmentsReady;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class MessageAssembler {
|
|
28
|
+
|
|
29
|
+
private _builders = new Map<number, MessageBuilder>();
|
|
30
|
+
|
|
31
|
+
public constructor(private _maxBuilderCount = 3) { }
|
|
32
|
+
|
|
33
|
+
public PushMessage(buffer: ArrayBuffer): ArrayBuffer | null {
|
|
34
|
+
let stream = new ArrayStream(buffer);
|
|
35
|
+
let messageIndex = stream.ReadUInt32();
|
|
36
|
+
let messageSize = stream.ReadUInt32();
|
|
37
|
+
let fragmentSize = stream.ReadUInt32();
|
|
38
|
+
let fragmentIndex = stream.ReadUInt32();
|
|
39
|
+
let fragment = new Uint8Array(stream.ReadToEnd());
|
|
40
|
+
|
|
41
|
+
let builder = this._builders.get(messageIndex);
|
|
42
|
+
if (builder === undefined) {
|
|
43
|
+
builder = new MessageBuilder(messageIndex, messageSize, fragmentSize);
|
|
44
|
+
this._builders.set(messageIndex, builder);
|
|
45
|
+
|
|
46
|
+
if (this._builders.size > this._maxBuilderCount) {
|
|
47
|
+
let [oldest] = this._builders.keys();
|
|
48
|
+
this._builders.delete(oldest);
|
|
49
|
+
console.log("Message lost.");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (builder.AddFragment(fragmentIndex, fragment)) {
|
|
54
|
+
this._builders.delete(messageIndex);
|
|
55
|
+
return builder.Buffer.buffer;
|
|
56
|
+
} else {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { EventOwner, EventPublisher, EventArgs } from "../infrastructure/events";
|
|
2
|
+
import { IMessagingClient } from "./messaging-client";
|
|
3
|
+
|
|
4
|
+
export class WebSocketClient<TMessage> implements IMessagingClient<TMessage> {
|
|
5
|
+
private readonly _socket: WebSocket;
|
|
6
|
+
private readonly _events = new EventOwner();
|
|
7
|
+
|
|
8
|
+
public readonly Connected = new EventPublisher<IMessagingClient<TMessage>, EventArgs>(this._events);
|
|
9
|
+
public readonly MessageReceived = new EventPublisher<IMessagingClient<TMessage>, TMessage>(this._events);
|
|
10
|
+
public readonly Disconnected = new EventPublisher<IMessagingClient<TMessage>, EventArgs>(this._events);
|
|
11
|
+
|
|
12
|
+
constructor(uri: string) {
|
|
13
|
+
console.log(`Connecting to ${uri}...`);
|
|
14
|
+
|
|
15
|
+
let socket = new WebSocket(uri);
|
|
16
|
+
socket.onopen = () => this.OnConnected();
|
|
17
|
+
socket.onmessage = (e: MessageEvent<string>) => this.OnMessageReceived(e);
|
|
18
|
+
socket.onclose = () => this.OnDisconnected();
|
|
19
|
+
socket.onerror = (e: Event) => this.OnError(e);
|
|
20
|
+
this._socket = socket;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
SendMessage(message: TMessage): void {
|
|
24
|
+
let content = JSON.stringify(message);
|
|
25
|
+
console.log(`Sending message: ${content}`);
|
|
26
|
+
|
|
27
|
+
this._socket.send(content);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
protected OnConnected(): void {
|
|
31
|
+
console.log(`Connected to: ${this._socket.url}.`);
|
|
32
|
+
this._events.Raise(this.Connected, this, EventArgs.Empty);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
protected OnMessageReceived(eventArgs: MessageEvent<string>): void {
|
|
36
|
+
console.log('Message received: ' + eventArgs.data);
|
|
37
|
+
|
|
38
|
+
let message = JSON.parse(eventArgs.data) as TMessage;
|
|
39
|
+
this._events.Raise(this.MessageReceived, this, message);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
protected OnDisconnected(): void {
|
|
43
|
+
console.log(`Disconnected from: ${this._socket.url}.`);
|
|
44
|
+
this._events.Raise(this.Disconnected, this, EventArgs.Empty);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
protected OnError(error: Event): void {
|
|
48
|
+
console.log('Connection failed: ' + error);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { TestBed } from '@angular/core/testing';
|
|
2
|
+
|
|
3
|
+
import { MessagingService } from './messaging.service';
|
|
4
|
+
|
|
5
|
+
describe('MessagingService', () => {
|
|
6
|
+
let service: MessagingService;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
TestBed.configureTestingModule({});
|
|
10
|
+
service = TestBed.inject(MessagingService);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should be created', () => {
|
|
14
|
+
expect(service).toBeTruthy();
|
|
15
|
+
});
|
|
16
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Injectable } from '@angular/core';
|
|
2
|
+
import { WebSocketClient } from '../networking/web-socket-client';
|
|
3
|
+
import { WarprSignalingMessage, ConnectionRequest } from '../data/signaling-messages';
|
|
4
|
+
|
|
5
|
+
@Injectable({
|
|
6
|
+
providedIn: 'root'
|
|
7
|
+
})
|
|
8
|
+
export class MessagingService extends WebSocketClient<WarprSignalingMessage> {
|
|
9
|
+
|
|
10
|
+
private static readonly _connectionUri = 'api/sinks/connect';
|
|
11
|
+
private readonly _sessionId = "a5d26e70-2b61-451f-9ecb-7d1b9a76ed1b";
|
|
12
|
+
|
|
13
|
+
constructor() {
|
|
14
|
+
const uri = MessagingService.GetServerUri();
|
|
15
|
+
super(uri);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
private static GetServerUri(): string {
|
|
19
|
+
const location = window.location;
|
|
20
|
+
const protocol = location.protocol === 'https:' ? 'wss' : 'ws';
|
|
21
|
+
return `${protocol}://${location.host}/${this._connectionUri}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
protected override OnConnected(): void {
|
|
25
|
+
super.OnConnected();
|
|
26
|
+
|
|
27
|
+
let request = new ConnectionRequest();
|
|
28
|
+
request.SessionId = this._sessionId;
|
|
29
|
+
this.SendMessage(request);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { TestBed } from '@angular/core/testing';
|
|
2
|
+
|
|
3
|
+
import { StreamingService } from './streaming.service';
|
|
4
|
+
|
|
5
|
+
describe('StreamingService', () => {
|
|
6
|
+
let service: StreamingService;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
TestBed.configureTestingModule({});
|
|
10
|
+
service = TestBed.inject(StreamingService);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should be created', () => {
|
|
14
|
+
expect(service).toBeTruthy();
|
|
15
|
+
});
|
|
16
|
+
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { Injectable } from '@angular/core';
|
|
2
|
+
import { MessagingService } from './messaging.service';
|
|
3
|
+
import { PeerConnectionCandidateMessage, PeerConnectionDescriptionMessage, WarprSignalingMessage, WarprSignalingMessageType } from '../data/signaling-messages';
|
|
4
|
+
import { IMessagingClient } from '../networking/messaging-client';
|
|
5
|
+
import { EncodedFrame, FrameType } from '../data/frames';
|
|
6
|
+
import { EventOwner, EventPublisher } from '../infrastructure/events';
|
|
7
|
+
import { MessageAssembler } from '../infrastructure/message-assembler';
|
|
8
|
+
|
|
9
|
+
@Injectable({
|
|
10
|
+
providedIn: 'root'
|
|
11
|
+
})
|
|
12
|
+
export class StreamingService {
|
|
13
|
+
|
|
14
|
+
private _peerConnection?: RTCPeerConnection;
|
|
15
|
+
private _reliableConnection?: RTCDataChannel;
|
|
16
|
+
private _lowLatencyConnection?: RTCDataChannel;
|
|
17
|
+
private _messageBuilder? : MessageAssembler;
|
|
18
|
+
|
|
19
|
+
private readonly _events = new EventOwner();
|
|
20
|
+
public readonly FrameReceived = new EventPublisher<StreamingService, EncodedFrame>(this._events);
|
|
21
|
+
public readonly Connected = new EventPublisher<StreamingService, any>(this._events);
|
|
22
|
+
|
|
23
|
+
private static _config: RTCConfiguration = {
|
|
24
|
+
iceServers: [
|
|
25
|
+
{ urls: "stun:stun.l.google.com:19302" },
|
|
26
|
+
{ urls: "stun:stun.relay.metered.ca:80" },
|
|
27
|
+
{
|
|
28
|
+
urls: "turn:global.relay.metered.ca:80",
|
|
29
|
+
username: "cb51ef701a61be3cdc89466f",
|
|
30
|
+
credential: "BBnGCvnTLaxhVbuf"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
urls: "turn:global.relay.metered.ca:80?transport=tcp",
|
|
34
|
+
username: "cb51ef701a61be3cdc89466f",
|
|
35
|
+
credential: "BBnGCvnTLaxhVbuf"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
urls: "turn:global.relay.metered.ca:443",
|
|
39
|
+
username: "cb51ef701a61be3cdc89466f",
|
|
40
|
+
credential: "BBnGCvnTLaxhVbuf"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
urls: "turns:global.relay.metered.ca:443?transport=tcp",
|
|
44
|
+
username: "cb51ef701a61be3cdc89466f",
|
|
45
|
+
credential: "BBnGCvnTLaxhVbuf"
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
constructor(
|
|
51
|
+
private _messagingService: MessagingService) {
|
|
52
|
+
|
|
53
|
+
_messagingService.MessageReceived.Subscribe((sender: IMessagingClient<WarprSignalingMessage>, message: WarprSignalingMessage) => this.OnMessageReceived(sender, message));
|
|
54
|
+
|
|
55
|
+
this.Reconnect();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private Reconnect() {
|
|
59
|
+
this._reliableConnection?.close();
|
|
60
|
+
this._lowLatencyConnection?.close();
|
|
61
|
+
this._peerConnection?.close();
|
|
62
|
+
this._messageBuilder = new MessageAssembler();
|
|
63
|
+
|
|
64
|
+
this._peerConnection = new RTCPeerConnection(StreamingService._config);
|
|
65
|
+
this._peerConnection.onicecandidate = (event) => this.OnIceCandidateAdded(event.candidate?.candidate);
|
|
66
|
+
this._peerConnection.ondatachannel = (event) => this.OnDataChannel(event);
|
|
67
|
+
this._peerConnection.onconnectionstatechange = (event) => this.OnConnectionStateChanged();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public SendMessage(message: any) {
|
|
71
|
+
let json = JSON.stringify(message);
|
|
72
|
+
this._reliableConnection?.send(json);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private OnConnectionStateChanged() {
|
|
76
|
+
console.log("WebRTC: " + this._peerConnection?.connectionState);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private OnDataChannel(event: RTCDataChannelEvent) {
|
|
80
|
+
console.log(`WebRTC data channel ${event.channel.label} connected.`);
|
|
81
|
+
|
|
82
|
+
switch (event.channel.label) {
|
|
83
|
+
case "reliable":
|
|
84
|
+
this._reliableConnection = event.channel;
|
|
85
|
+
this._events.Raise(this.Connected, this, null);
|
|
86
|
+
break;
|
|
87
|
+
case "low_latency":
|
|
88
|
+
this._lowLatencyConnection = event.channel;
|
|
89
|
+
this._lowLatencyConnection.binaryType = "arraybuffer"
|
|
90
|
+
this._lowLatencyConnection.onmessage = (event) => this.OnLowLatencyMessage(event);
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private _lastRefreshTime = performance.now();
|
|
96
|
+
private _count = 0;
|
|
97
|
+
|
|
98
|
+
private OnLowLatencyMessage(event: MessageEvent<any>) {
|
|
99
|
+
|
|
100
|
+
let message = this._messageBuilder?.PushMessage(event.data as ArrayBuffer);
|
|
101
|
+
if (!message) return;
|
|
102
|
+
|
|
103
|
+
let now = performance.now();
|
|
104
|
+
let elapsed = now - this._lastRefreshTime;
|
|
105
|
+
this._count++;
|
|
106
|
+
|
|
107
|
+
if (elapsed > 1000) {
|
|
108
|
+
this._lastRefreshTime = now;
|
|
109
|
+
|
|
110
|
+
console.log(`Messages: ${this._count / elapsed * 1000} FPS`);
|
|
111
|
+
this._count = 0;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let frame = new EncodedFrame(message);
|
|
115
|
+
this._events.Raise(this.FrameReceived, this, frame);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private OnIceCandidateAdded(candidate?: string) {
|
|
119
|
+
if (candidate == null) return;
|
|
120
|
+
|
|
121
|
+
let message = new PeerConnectionCandidateMessage();
|
|
122
|
+
message.Candidate = candidate;
|
|
123
|
+
this._messagingService.SendMessage(message);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private async OnMessageReceived(sender: IMessagingClient<WarprSignalingMessage>, message: WarprSignalingMessage) {
|
|
127
|
+
|
|
128
|
+
if (!this._peerConnection) return;
|
|
129
|
+
|
|
130
|
+
switch (message.$type) {
|
|
131
|
+
case WarprSignalingMessageType.PairingCompleteMessage:
|
|
132
|
+
this.Reconnect();
|
|
133
|
+
break;
|
|
134
|
+
case WarprSignalingMessageType.PeerConnectionDescriptionMessage:
|
|
135
|
+
await this._peerConnection.setRemoteDescription({ type: "offer", sdp: message.Description });
|
|
136
|
+
let answer = await this._peerConnection.createAnswer();
|
|
137
|
+
await this._peerConnection.setLocalDescription(answer);
|
|
138
|
+
|
|
139
|
+
let answerMessage = new PeerConnectionDescriptionMessage();
|
|
140
|
+
answerMessage.Description = answer.sdp;
|
|
141
|
+
this._messagingService.SendMessage(answerMessage);
|
|
142
|
+
|
|
143
|
+
break;
|
|
144
|
+
case WarprSignalingMessageType.PeerConnectionCandidateMessage:
|
|
145
|
+
await this._peerConnection.addIceCandidate({ candidate: message.Candidate, sdpMid: "0" });
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Public API Surface of warpr-lib
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export * from './lib/data/frames';
|
|
6
|
+
export * from './lib/data/signaling-messages';
|
|
7
|
+
export * from './lib/data/streaming-messages';
|
|
8
|
+
export * from './lib/infrastructure/array-stream';
|
|
9
|
+
export * from './lib/infrastructure/events';
|
|
10
|
+
export * from './lib/infrastructure/message-assembler';
|
|
11
|
+
export * from './lib/networking/messaging-client';
|
|
12
|
+
export * from './lib/networking/web-socket-client';
|
|
13
|
+
export * from './lib/services/messaging.service';
|
|
14
|
+
export * from './lib/services/streaming.service';
|
|
15
|
+
export * from './lib/components/stream-host/stream-host.component';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
|
2
|
+
{
|
|
3
|
+
"extends": "../../tsconfig.json",
|
|
4
|
+
"compilerOptions": {
|
|
5
|
+
"outDir": "../../out-tsc/lib",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"declarationMap": true,
|
|
8
|
+
"inlineSources": true,
|
|
9
|
+
"types": []
|
|
10
|
+
},
|
|
11
|
+
"exclude": [
|
|
12
|
+
"**/*.spec.ts"
|
|
13
|
+
]
|
|
14
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
|
2
|
+
{
|
|
3
|
+
"extends": "../../tsconfig.json",
|
|
4
|
+
"compilerOptions": {
|
|
5
|
+
"outDir": "../../out-tsc/spec",
|
|
6
|
+
"types": [
|
|
7
|
+
"jasmine"
|
|
8
|
+
]
|
|
9
|
+
},
|
|
10
|
+
"include": [
|
|
11
|
+
"**/*.spec.ts",
|
|
12
|
+
"**/*.d.ts"
|
|
13
|
+
]
|
|
14
|
+
}
|