@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.
- package/.eslintignore +1 -0
- package/.eslintrc.js +22 -0
- package/.gitlab-ci.yml +42 -0
- package/README.md +1 -0
- package/dist/MungeSdp.d.ts +2 -0
- package/dist/Types/CodecsOptions.d.ts +8 -0
- package/dist/Types/CommonConnectionTypes.d.ts +48 -0
- package/dist/Types/EnumCodecContentType.d.ts +13 -0
- package/dist/Types/WebRTCStreamConfig.d.ts +26 -0
- package/dist/WebRtcInputConnection.d.ts +19 -0
- package/dist/WebRtcOutputConnection.d.ts +20 -0
- package/dist/WebRtcPeerConnection.d.ts +23 -0
- package/dist/browserSupportedAudioCodec.d.ts +13 -0
- package/dist/browserSupportedVideoCodec.d.ts +7 -0
- package/dist/stats.json +10326 -0
- package/dist/utils.d.ts +6 -0
- package/dist/version.d.ts +1 -0
- package/dist/vxwebrtc.d.ts +6 -0
- package/dist/vxwebrtc.js +3 -0
- package/dist/vxwebrtc.js.LICENSE.txt +8 -0
- package/dist/vxwebrtc.js.map +1 -0
- package/package.json +53 -0
- package/src/MungeSdp.ts +323 -0
- package/src/Types/CodecsOptions.ts +9 -0
- package/src/Types/CommonConnectionTypes.ts +57 -0
- package/src/Types/EnumCodecContentType.ts +13 -0
- package/src/Types/WebRTCStreamConfig.ts +59 -0
- package/src/WebRtcInputConnection.ts +152 -0
- package/src/WebRtcOutputConnection.ts +122 -0
- package/src/WebRtcPeerConnection.ts +91 -0
- package/src/browserSupportedAudioCodec.ts +49 -0
- package/src/browserSupportedVideoCodec.ts +39 -0
- package/src/utils.ts +18 -0
- package/src/version.ts +1 -0
- package/src/vxwebrtc.ts +8 -0
- package/tsconfig.json +15 -0
- package/tslint.json +18 -0
- package/webpack.common.js +43 -0
- package/webpack.dev.js +7 -0
- 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
|
+
}
|
package/src/MungeSdp.ts
ADDED
|
@@ -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,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
|
+
}
|