@campoint/vxwebrtc 0.0.2

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 (40) hide show
  1. package/.eslintignore +1 -0
  2. package/.eslintrc.js +22 -0
  3. package/.gitlab-ci.yml +42 -0
  4. package/README.md +1 -0
  5. package/dist/MungeSdp.d.ts +2 -0
  6. package/dist/Types/CodecsOptions.d.ts +8 -0
  7. package/dist/Types/CommonConnectionTypes.d.ts +48 -0
  8. package/dist/Types/EnumCodecContentType.d.ts +13 -0
  9. package/dist/Types/WebRTCStreamConfig.d.ts +26 -0
  10. package/dist/WebRtcInputConnection.d.ts +19 -0
  11. package/dist/WebRtcOutputConnection.d.ts +20 -0
  12. package/dist/WebRtcPeerConnection.d.ts +23 -0
  13. package/dist/browserSupportedAudioCodec.d.ts +13 -0
  14. package/dist/browserSupportedVideoCodec.d.ts +7 -0
  15. package/dist/stats.json +10326 -0
  16. package/dist/utils.d.ts +6 -0
  17. package/dist/version.d.ts +1 -0
  18. package/dist/vxwebrtc.d.ts +6 -0
  19. package/dist/vxwebrtc.js +3 -0
  20. package/dist/vxwebrtc.js.LICENSE.txt +8 -0
  21. package/dist/vxwebrtc.js.map +1 -0
  22. package/package.json +53 -0
  23. package/src/MungeSdp.ts +323 -0
  24. package/src/Types/CodecsOptions.ts +9 -0
  25. package/src/Types/CommonConnectionTypes.ts +57 -0
  26. package/src/Types/EnumCodecContentType.ts +13 -0
  27. package/src/Types/WebRTCStreamConfig.ts +59 -0
  28. package/src/WebRtcInputConnection.ts +152 -0
  29. package/src/WebRtcOutputConnection.ts +122 -0
  30. package/src/WebRtcPeerConnection.ts +91 -0
  31. package/src/browserSupportedAudioCodec.ts +49 -0
  32. package/src/browserSupportedVideoCodec.ts +39 -0
  33. package/src/utils.ts +18 -0
  34. package/src/version.ts +1 -0
  35. package/src/vxwebrtc.ts +8 -0
  36. package/tsconfig.json +15 -0
  37. package/tslint.json +18 -0
  38. package/webpack.common.js +43 -0
  39. package/webpack.dev.js +7 -0
  40. package/webpack.prod.js +7 -0
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@campoint/vxwebrtc",
3
+ "version": "0.0.2",
4
+ "main": "./dist/vxwebrtc.js",
5
+ "types": "./dist/vxwebrtc.d.ts",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+ssh://git@gitlab.com/CampointAG/web-rtc.git"
9
+ },
10
+ "author": "evgeniy@aporia.su",
11
+ "license": "UNLICENSED",
12
+ "dependencies": {
13
+ "lodash": "^4.17.21",
14
+ "webrtc-adapter": "^8.1.1"
15
+ },
16
+ "publishConfig": {
17
+ "registry": "https://registry.npmjs.org"
18
+ },
19
+ "scripts": {
20
+ "clean": "rimraf ./dist",
21
+ "clean:deps": "rimraf ./node_modules",
22
+ "build": "webpack --config webpack.prod.js",
23
+ "build:dev": "webpack --config webpack.dev.js",
24
+ "watch": "webpack --config webpack.dev.js --watch",
25
+ "lint": "eslint src/ --ext .js,.jsx,.ts,.tsx,.json",
26
+ "style": "prettier --write \"{src,test}/**/*.ts\""
27
+ },
28
+ "prettier": {
29
+ "singleQuote": true,
30
+ "arrowParens": "always",
31
+ "endOfLine": "lf",
32
+ "printWidth": 100
33
+ },
34
+ "devDependencies": {
35
+ "@typescript-eslint/eslint-plugin": "^5.12.0",
36
+ "@typescript-eslint/parser": "^5.12.0",
37
+ "clean-webpack-plugin": "^4.0.0",
38
+ "duplicate-package-checker-webpack-plugin": "^3.0.0",
39
+ "eslint": "^8.9.0",
40
+ "prettier": "^2.5.1",
41
+ "ts-loader": "^9.2.6",
42
+ "typescript": "^4.5.5",
43
+ "webpack": "^5.69.0",
44
+ "webpack-bundle-analyzer": "^4.5.0",
45
+ "webpack-cli": "^4.9.2",
46
+ "webpack-merge": "^5.8.0"
47
+ },
48
+ "description": "",
49
+ "bugs": {
50
+ "url": "https://gitlab.com/CampointAG/web-rtc/issues"
51
+ },
52
+ "homepage": "https://gitlab.com/CampointAG/web-rtc#readme"
53
+ }
@@ -0,0 +1,323 @@
1
+ //@link https://www.wowza.com/wp-content/themes/wowzav1/webrtc-ui/wordpress-dev/1.2.9/lib/WowzaMungeSDP.js
2
+ //no refactoring required, in order to facilitate updates
3
+ import * as _ from 'lodash';
4
+ import adapter from 'webrtc-adapter';
5
+ import { MediaInfo, EnumVideoCodec, EnumMediaStreamTrackKind } from './Types/CommonConnectionTypes';
6
+ import { EnumVideoCodecs } from './browserSupportedVideoCodec';
7
+
8
+ type TSDPOutput = {
9
+ [key: number]: string;
10
+ };
11
+
12
+ let SDPOutput: TSDPOutput = {};
13
+ let audioChoice = '';
14
+ let videoIndex = -1;
15
+ let audioIndex = -1;
16
+
17
+ function addAudio(sdpStr: string, audioLine: string) {
18
+ let sdpLines = sdpStr.split(/\r\n/);
19
+ let sdpStrRet = '';
20
+ let done = false;
21
+ let sdpSection = '';
22
+
23
+ for (const sdpLine of sdpLines) {
24
+ if (sdpLine.length <= 0) {
25
+ continue;
26
+ }
27
+
28
+ if (sdpLine.indexOf('m=audio') === 0) {
29
+ sdpSection = 'audio';
30
+ } else if (sdpLine.indexOf('m=video') === 0) {
31
+ sdpSection = 'video';
32
+ }
33
+
34
+ sdpStrRet += sdpLine;
35
+ sdpStrRet += '\r\n';
36
+
37
+ if (sdpSection === 'audio' && 'a=rtcp-mux' === sdpLine && !done) {
38
+ sdpStrRet += audioLine;
39
+ done = true;
40
+ }
41
+ }
42
+ return sdpStrRet;
43
+ }
44
+
45
+ function addVideo(sdpStr: string, videoLine: string) {
46
+ let sdpLines = sdpStr.split(/\r\n/);
47
+ let sdpStrRet = '';
48
+ let done = false;
49
+
50
+ let rtcpSize = false;
51
+
52
+ for (const sdpLine of sdpLines) {
53
+ if (sdpLine.length <= 0) continue;
54
+
55
+ if (sdpLine.includes('a=rtcp-rsize')) {
56
+ rtcpSize = true;
57
+ }
58
+
59
+ // if (sdpLine.includes("a=rtcp-mux")) {
60
+ // rtcpMux = true;
61
+ // }
62
+ }
63
+
64
+ for (const sdpLine of sdpLines) {
65
+ sdpStrRet += sdpLine;
66
+ sdpStrRet += '\r\n';
67
+
68
+ if ('a=rtcp-rsize'.localeCompare(sdpLine) === 0 && !done && rtcpSize) {
69
+ sdpStrRet += videoLine;
70
+ done = true;
71
+ }
72
+
73
+ if ('a=rtcp-mux'.localeCompare(sdpLine) === 0 && done && !rtcpSize) {
74
+ sdpStrRet += videoLine;
75
+ done = true;
76
+ }
77
+
78
+ if ('a=rtcp-mux'.localeCompare(sdpLine) === 0 && !done && !rtcpSize) {
79
+ done = true;
80
+ }
81
+ }
82
+ return sdpStrRet;
83
+ }
84
+
85
+ // Filter codec offerings
86
+ function deliverCheckLine(profile: string, type: EnumMediaStreamTrackKind): string {
87
+ for (let l in SDPOutput) {
88
+ const line = Number.parseInt(l, 10);
89
+ let lineInUse = SDPOutput[line];
90
+ if (lineInUse.includes(profile)) {
91
+ if (profile.includes('VP9') || profile.includes('VP8')) {
92
+ let output = '';
93
+ let outputs = lineInUse.split(/\r\n/);
94
+ for (const transport of outputs) {
95
+ // NOTE: This block of code is needed for WSE versions older than 4.8.5
96
+ // if (transport.indexOf("a=extmap") !== -1 ||
97
+ // transport.indexOf("transport-cc") !== -1 ||
98
+ // transport.indexOf("goog-remb") !== -1 ||
99
+ // transport.indexOf("nack") !== -1) {
100
+ // continue;
101
+ // }
102
+ output += transport;
103
+ output += '\r\n';
104
+ }
105
+
106
+ if (type.includes(EnumMediaStreamTrackKind.AUDIO)) {
107
+ audioIndex = line;
108
+ }
109
+
110
+ if (type.includes(EnumMediaStreamTrackKind.VIDEO)) {
111
+ videoIndex = line;
112
+ }
113
+
114
+ return output;
115
+ }
116
+ if (type.includes(EnumMediaStreamTrackKind.AUDIO)) {
117
+ audioIndex = line;
118
+ }
119
+
120
+ if (type.includes(EnumMediaStreamTrackKind.VIDEO)) {
121
+ videoIndex = line;
122
+ }
123
+
124
+ return lineInUse;
125
+ }
126
+ }
127
+ return '';
128
+ }
129
+
130
+ function checkLine(line: string) {
131
+ if (line.startsWith('a=rtpmap') || line.startsWith('a=rtcp-fb') || line.startsWith('a=fmtp')) {
132
+ let res = line.split(':');
133
+
134
+ if (res.length > 1) {
135
+ let number = res[1].split(' ');
136
+ const pos = Number.parseInt(number[0], 10);
137
+ if (!Number.isNaN(pos)) {
138
+ if (!number[1].startsWith('http') && !number[1].startsWith('ur')) {
139
+ let currentString = SDPOutput[pos];
140
+ if (!currentString) {
141
+ currentString = '';
142
+ }
143
+ currentString += line + '\r\n';
144
+ SDPOutput[pos] = currentString;
145
+ return false;
146
+ }
147
+ }
148
+ }
149
+ }
150
+
151
+ return true;
152
+ }
153
+
154
+ function getRtpMapId(line: string) {
155
+ const findId = new RegExp('a=rtpmap:(\\d+) (\\w+)/(\\d+)');
156
+ const found = line.match(findId);
157
+ return found && found.length >= 3 ? found : null;
158
+ }
159
+
160
+ export function mungeSdpPublish(sdpStr: string, mungeData: MediaInfo) {
161
+ SDPOutput = {};
162
+ audioChoice = !_.isEmpty(mungeData.audioCodec) ? mungeData.audioCodec : 'opus';
163
+ videoIndex = -1;
164
+ audioIndex = -1;
165
+
166
+ let sdpLines = sdpStr.split(/\r\n/);
167
+
168
+ let sdpSection = 'header';
169
+ let hitMID = false;
170
+ let sdpStrRet = '';
171
+ const browserName = adapter.browserDetails.browser;
172
+
173
+ // Deliver the requested codecs
174
+ for (const sdpLine of sdpLines) {
175
+ if (sdpLine.length <= 0) continue;
176
+
177
+ if (!checkLine(sdpLine)) {
178
+ continue;
179
+ }
180
+
181
+ sdpStrRet += sdpLine;
182
+ sdpStrRet += '\r\n';
183
+ }
184
+
185
+ sdpStrRet = addAudio(sdpStrRet, deliverCheckLine(audioChoice, EnumMediaStreamTrackKind.AUDIO));
186
+ sdpStrRet = addVideo(
187
+ sdpStrRet,
188
+ deliverCheckLine(mungeData.videoCodec, EnumMediaStreamTrackKind.VIDEO)
189
+ );
190
+ sdpStr = sdpStrRet;
191
+ sdpLines = sdpStr.split(/\r\n/);
192
+ sdpStrRet = '';
193
+
194
+ for (let sdpLine of sdpLines) {
195
+ if (sdpLine.length <= 0) continue;
196
+
197
+ if (browserName === 'chrome') {
198
+ let audioMLines;
199
+ if (sdpLine.indexOf('m=audio') === 0 && audioIndex !== -1) {
200
+ audioMLines = sdpLine.split(' ');
201
+ sdpStrRet +=
202
+ audioMLines[0] + ' ' + audioMLines[1] + ' ' + audioMLines[2] + ' ' + audioIndex + '\r\n';
203
+ continue;
204
+ }
205
+
206
+ if (sdpLine.indexOf('m=video') === 0 && videoIndex !== -1) {
207
+ audioMLines = sdpLine.split(' ');
208
+ sdpStrRet +=
209
+ audioMLines[0] + ' ' + audioMLines[1] + ' ' + audioMLines[2] + ' ' + videoIndex + '\r\n';
210
+ continue;
211
+ }
212
+ }
213
+
214
+ if (mungeData.videoCodec === EnumVideoCodec.H264 && sdpLine.startsWith('a=fmtp')) {
215
+ if (_.has(mungeData.h264CodecOptions, 'packetization-mode')) {
216
+ sdpLine = sdpLine.replace(
217
+ /packetization-mode[\s\d=]+/,
218
+ `packetization-mode=${_.get(mungeData.h264CodecOptions, 'packetization-mode')}`
219
+ );
220
+ }
221
+
222
+ if (_.has(mungeData.h264CodecOptions, 'profile-level-id')) {
223
+ sdpLine = sdpLine.replace(
224
+ /profile-level-id[\s=]+[^;]+/,
225
+ `profile-level-id=${_.get(mungeData.h264CodecOptions, 'profile-level-id')}`
226
+ );
227
+ }
228
+
229
+ if (_.has(mungeData.h264CodecOptions, 'level-asymmetry-allowed')) {
230
+ if (sdpLine.indexOf('level-asymmetry-allowed') !== -1) {
231
+ sdpLine = sdpLine.replace(
232
+ /level-asymmetry-allowed[\s=]+[^;]+/,
233
+ `level-asymmetry-allowed=${_.get(
234
+ mungeData.h264CodecOptions,
235
+ 'level-asymmetry-allowed'
236
+ )}`
237
+ );
238
+ }
239
+ }
240
+ } else if (
241
+ mungeData.videoCodec === EnumVideoCodecs.VP9 &&
242
+ sdpLine.startsWith('a=fmtp') &&
243
+ sdpLine.indexOf('profile-id') !== -1
244
+ ) {
245
+ if (_.has(mungeData.vp9CodecOptions, 'profile-id')) {
246
+ sdpLine = sdpLine.replace(
247
+ /profile-id[\s=]+[^;]+/,
248
+ `profile-id=${_.get(mungeData.vp9CodecOptions, 'profile-id')}`
249
+ );
250
+ }
251
+ }
252
+ sdpStrRet += sdpLine;
253
+
254
+ if (sdpLine.indexOf('m=audio') === 0) {
255
+ sdpSection = 'audio';
256
+ hitMID = false;
257
+ } else if (sdpLine.indexOf('m=video') === 0) {
258
+ sdpSection = 'video';
259
+ hitMID = false;
260
+ } else if (sdpLine.indexOf('a=rtpmap') === 0) {
261
+ sdpSection = 'bandwidth';
262
+ hitMID = false;
263
+ }
264
+
265
+ if (browserName === 'chrome') {
266
+ if (sdpLine.indexOf('a=mid:') === 0 || sdpLine.indexOf('a=rtpmap') === 0) {
267
+ if (!hitMID) {
268
+ if ('audio' === sdpSection) {
269
+ sdpStrRet += '\r\nb=CT:' + mungeData.audioBitrate.max;
270
+ sdpStrRet += '\r\nb=AS:' + mungeData.audioBitrate.min;
271
+ hitMID = true;
272
+ } else if ('video' === sdpSection) {
273
+ sdpStrRet += '\r\nb=CT:' + mungeData.videoBitrate.max;
274
+ sdpStrRet += '\r\nb=AS:' + mungeData.videoBitrate.min;
275
+ sdpStrRet += '\r\na=framerate:' + _.get(mungeData.videoFrameRate, 'ideal', 30);
276
+ hitMID = true;
277
+ } else if ('bandwidth' === sdpSection) {
278
+ const rtpmapID = getRtpMapId(sdpLine);
279
+ if (rtpmapID !== null) {
280
+ const match = rtpmapID[2].toLowerCase();
281
+ if (_.includes(['vp9', 'vp8', 'h264', 'red', 'ulpfec', 'rtx'], match)) {
282
+ sdpStrRet +=
283
+ '\r\na=fmtp:' +
284
+ rtpmapID[1] +
285
+ ' x-google-min-bitrate=' +
286
+ mungeData.videoBitrate.min +
287
+ ';x-google-max-bitrate=' +
288
+ mungeData.videoBitrate.max;
289
+ }
290
+
291
+ if (_.includes(['opus', 'isac', 'g722', 'pcmu', 'pcma', 'cn'], match)) {
292
+ sdpStrRet +=
293
+ '\r\na=fmtp:' +
294
+ rtpmapID[1] +
295
+ ' x-google-min-bitrate=' +
296
+ mungeData.audioBitrate.min +
297
+ ';x-google-max-bitrate=' +
298
+ mungeData.audioBitrate.max;
299
+ }
300
+ }
301
+ }
302
+ }
303
+ }
304
+ }
305
+
306
+ if (browserName === 'firefox' || browserName === 'safari') {
307
+ if (sdpLine.indexOf('c=IN') === 0) {
308
+ if ('audio' === sdpSection) {
309
+ const audioBitrateTIAS = Math.round(
310
+ mungeData.audioBitrate.min * 0.95 - (50 * 40 * 8) / 1000
311
+ );
312
+ sdpStrRet += '\r\nb=TIAS:' + audioBitrateTIAS + '\r\n';
313
+ sdpStrRet += 'b=AS:' + mungeData.audioBitrate.min + '\r\n';
314
+ sdpStrRet += 'b=CT:' + mungeData.audioBitrate.max + '\r\n';
315
+ continue;
316
+ }
317
+ }
318
+ }
319
+
320
+ sdpStrRet += '\r\n';
321
+ }
322
+ return sdpStrRet;
323
+ }
@@ -0,0 +1,9 @@
1
+ export interface TWebRtcH264CodecOptions {
2
+ 'packetization-mode'?: 0 | 1;
3
+ 'profile-level-id'?: string;
4
+ 'level-asymmetry-allowed'?: number;
5
+ }
6
+
7
+ export interface TWebRtcVP9CodecOptions {
8
+ 'profile-id'?: number;
9
+ }
@@ -0,0 +1,57 @@
1
+ import { TConstrainULongRange } from './WebRTCStreamConfig';
2
+ import { TWebRtcH264CodecOptions, TWebRtcVP9CodecOptions } from './CodecsOptions';
3
+
4
+ export type OnErrorCallback = (e: Error) => void;
5
+ export type OnStopCallback = () => void;
6
+
7
+ export interface StreamInfo {
8
+ applicationName: string;
9
+ streamName: string;
10
+ sessionId: string;
11
+ }
12
+
13
+ export interface WebSocketReply {
14
+ status: string;
15
+ statusDescription: string;
16
+ command: string;
17
+ streamInfo?: StreamInfo;
18
+ sdp?: RTCSessionDescriptionInit;
19
+ iceCandidates?: RTCIceCandidateInit[];
20
+ direction: string;
21
+ }
22
+
23
+ export enum EnumVideoCodec {
24
+ H264 = '42e01f',
25
+ }
26
+
27
+ export interface MediaInfo {
28
+ audioBitrate: TConstrainULongRange;
29
+ audioCodec: string;
30
+ videoBitrate: TConstrainULongRange;
31
+ videoCodec: string;
32
+ videoFrameRate: ConstrainULongRange;
33
+ h264CodecOptions: TWebRtcH264CodecOptions;
34
+ vp9CodecOptions: TWebRtcVP9CodecOptions;
35
+ }
36
+
37
+ export enum EnumWebSocketConnectionStatus {
38
+ OK = 200,
39
+ NOT_READY = 514,
40
+ }
41
+
42
+ export enum EnumWebSocketConnectionDirection {
43
+ PUBLISH = 'publish',
44
+ PLAY = 'play',
45
+ }
46
+
47
+ export enum EnumWebSocketConnectionCommand {
48
+ SEND_OFFER = 'sendOffer',
49
+ GET_OFFER = 'getOffer',
50
+ SEND_RESPONSE = 'sendResponse',
51
+ GET_AVAILABLE_STREAM = 'getAvailableStreams',
52
+ }
53
+
54
+ export enum EnumMediaStreamTrackKind {
55
+ AUDIO = 'audio',
56
+ VIDEO = 'video',
57
+ }
@@ -0,0 +1,13 @@
1
+ export enum EnumCodecContentType {
2
+ OPUS = 'audio/mp4;codecs="opus"',
3
+ VORBIS = 'audio/ogg;codecs="vorbis"',
4
+ H264 = 'video/mp4',
5
+ VP8 = 'video/webm;codecs="vp8"',
6
+ VP9 = 'video/webm;codecs="vp9"',
7
+ WEBM = 'video/webm',
8
+ HLS = 'application/vnd.apple.mpegURL',
9
+ H264_AVC = 'video/mp4; codecs="avc1.42E01E"',
10
+ H264_MP4 = 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"',
11
+ H265_HEC = 'video/mp4; codecs="hvc1.1.L0.0"',
12
+ H265_HEV = 'video/mp4; codecs="hev1.1.L0.0"',
13
+ }
@@ -0,0 +1,59 @@
1
+ export abstract class WebRTCStreamConfig {
2
+ url = '';
3
+ applicationName = '';
4
+ streamName = '';
5
+
6
+ h264CodecOptions = {};
7
+ vp9CodecOptions = {};
8
+
9
+ video: TVideoPermissions = {
10
+ width: {
11
+ min: 1024,
12
+ ideal: 1280,
13
+ max: 1920,
14
+ },
15
+ height: {
16
+ min: 576,
17
+ ideal: 720,
18
+ max: 1080,
19
+ },
20
+ frameRate: {
21
+ min: 15,
22
+ ideal: 30,
23
+ max: 30,
24
+ },
25
+ bitRate: {
26
+ min: 800000,
27
+ ideal: 2500000,
28
+ max: 3500000,
29
+ },
30
+ };
31
+
32
+ audio: TAudioPermissions = {
33
+ bitRate: {
34
+ min: 32000,
35
+ ideal: 64000,
36
+ max: 128000,
37
+ },
38
+ };
39
+ }
40
+
41
+ export type TAudioPermissions = {
42
+ noiseSuppression?: boolean;
43
+ echoCancellation?: boolean;
44
+ channelCount?: number;
45
+ bitRate: TConstrainULongRange;
46
+ };
47
+
48
+ export type TVideoPermissions = {
49
+ width: TConstrainULongRange;
50
+ height: TConstrainULongRange;
51
+ frameRate: TConstrainULongRange;
52
+ bitRate: TConstrainULongRange;
53
+ };
54
+
55
+ export type TConstrainULongRange = {
56
+ min: number;
57
+ ideal: number;
58
+ max: number;
59
+ };
@@ -0,0 +1,152 @@
1
+ import {
2
+ EnumWebSocketConnectionCommand,
3
+ EnumWebSocketConnectionDirection,
4
+ WebSocketReply,
5
+ EnumWebSocketConnectionStatus,
6
+ } from './Types/CommonConnectionTypes';
7
+ import WebRtcPeerConnection, { IWebRtcPeerConnectionOptions } from './WebRtcPeerConnection';
8
+
9
+ const REPEATER_RETRY_COUNTER = 10;
10
+
11
+ export class WebRtcInputConnection extends WebRtcPeerConnection {
12
+ protected connectionOptions: IWebRtcInputConnection;
13
+ private _peerConnectionConfig?: RTCConfiguration = undefined;
14
+ private _repeaterRetryCount = 0;
15
+ private _stream: MediaStream | undefined;
16
+
17
+ constructor(connectionOptions: IWebRtcInputConnection) {
18
+ super(connectionOptions);
19
+
20
+ this.connectionOptions = connectionOptions;
21
+ this.onRtcDescription = this.onRtcDescription.bind(this);
22
+ this.onWsOpen = this.onWsOpen.bind(this);
23
+ this.onWsMessage = this.onWsMessage.bind(this);
24
+ this.onError = this.onError.bind(this);
25
+ this.onRemoteTrack = this.onRemoteTrack.bind(this);
26
+ }
27
+
28
+ protected onRtcDescription(description: RTCSessionDescriptionInit) {
29
+ if (!this.peerConnection) {
30
+ return this.onError(new Error('RTC got description, but no RTC'));
31
+ }
32
+ this.peerConnection
33
+ .setLocalDescription(description)
34
+ .then(() =>
35
+ this.sendResponse(
36
+ EnumWebSocketConnectionDirection.PLAY,
37
+ EnumWebSocketConnectionCommand.SEND_RESPONSE,
38
+ description
39
+ )
40
+ )
41
+ .catch(this.onError);
42
+ }
43
+
44
+ protected onRemoteTrack(event: RTCTrackEvent): void {
45
+ try {
46
+ this._stream = event.streams[0];
47
+ this.connectionOptions.onTrack!(event.track.kind);
48
+ if (this.connectionOptions.videoElement) {
49
+ this.connectionOptions.videoElement.srcObject = event.streams[0];
50
+ }
51
+ } catch (error) {
52
+ this.onError(new Error(JSON.stringify(error ? error : '')));
53
+ }
54
+ }
55
+
56
+ protected onWsOpen(): void {
57
+ this.peerConnection = new RTCPeerConnection(this._peerConnectionConfig);
58
+ this.peerConnection.ontrack = this.onRemoteTrack;
59
+
60
+ this.sendResponse(
61
+ EnumWebSocketConnectionDirection.PLAY,
62
+ EnumWebSocketConnectionCommand.GET_OFFER
63
+ );
64
+ }
65
+
66
+ protected onWsMessage(evt: MessageEvent) {
67
+ const msgJson: WebSocketReply = JSON.parse(evt.data);
68
+ const msgStatus = parseInt(msgJson.status, 10);
69
+
70
+ if (msgStatus !== EnumWebSocketConnectionStatus.OK) {
71
+ this._repeaterRetryCount++;
72
+
73
+ if (this._repeaterRetryCount < REPEATER_RETRY_COUNTER) {
74
+ setTimeout(
75
+ () =>
76
+ this.sendResponse(
77
+ EnumWebSocketConnectionDirection.PLAY,
78
+ EnumWebSocketConnectionCommand.GET_OFFER
79
+ ),
80
+ this.connectionOptions.debounceTime
81
+ );
82
+ } else {
83
+ this.onError(
84
+ new Error('Live stream repeater timeout: ' + this.connectionOptions.streamInfo.streamName)
85
+ );
86
+ this.stop();
87
+ }
88
+ } else {
89
+ const sdpData = msgJson.sdp;
90
+ const streamInfoResponse = msgJson.streamInfo;
91
+
92
+ if (streamInfoResponse !== undefined) {
93
+ this.connectionOptions.streamInfo.sessionId = streamInfoResponse.sessionId;
94
+ }
95
+
96
+ if (sdpData && this.peerConnection) {
97
+ this.peerConnection
98
+ .setRemoteDescription(new RTCSessionDescription(sdpData))
99
+ .then(() => {
100
+ if (this.peerConnection !== undefined) {
101
+ return this.peerConnection
102
+ .createAnswer()
103
+ .then((description: RTCSessionDescriptionInit) =>
104
+ this.onRtcDescription(description)
105
+ )
106
+ .catch(this.onError);
107
+ }
108
+ })
109
+ .catch(this.onError);
110
+ }
111
+
112
+ const iceCandidates: RTCIceCandidateInit[] = msgJson.iceCandidates;
113
+
114
+ if (iceCandidates?.length > 0 && this.peerConnection) {
115
+ for (const iceCandidate of iceCandidates) {
116
+ this.peerConnection
117
+ .addIceCandidate(new RTCIceCandidate(iceCandidate))
118
+ .catch(this.onError);
119
+ }
120
+ }
121
+ }
122
+
123
+ if (this.wsConnection && 'sendResponse'.localeCompare(msgJson.command) === 0) {
124
+ if (this.wsConnection !== null) {
125
+ this.wsConnection.close();
126
+ }
127
+
128
+ this.wsConnection = undefined;
129
+ }
130
+ }
131
+
132
+ public getStream(): MediaStream {
133
+ return this._stream;
134
+ }
135
+
136
+ public start(): void {
137
+ super.start();
138
+
139
+ this._repeaterRetryCount = 0;
140
+
141
+ if (this.wsConnection) {
142
+ this.wsConnection.onopen = this.onWsOpen;
143
+ this.wsConnection.onmessage = this.onWsMessage;
144
+ }
145
+ }
146
+ }
147
+
148
+ export interface IWebRtcInputConnection extends IWebRtcPeerConnectionOptions {
149
+ videoElement?: HTMLVideoElement;
150
+ onTrack?: (string) => void;
151
+ debounceTime: number;
152
+ }