@flashphoner/sfusdk 1.0.1-35 → 1.0.40

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/Gruntfile.js CHANGED
@@ -50,6 +50,18 @@ module.exports = function(grunt) {
50
50
  release: [
51
51
  'release'
52
52
  ]
53
+ },
54
+ run: {
55
+ options: {
56
+ // ...
57
+ },
58
+ test: {
59
+ cmd: 'npm',
60
+ args: [
61
+ 'run',
62
+ 'test'
63
+ ]
64
+ }
53
65
  }
54
66
  });
55
67
 
@@ -57,6 +69,7 @@ module.exports = function(grunt) {
57
69
  grunt.loadNpmTasks('grunt-contrib-copy');
58
70
  grunt.loadNpmTasks('grunt-contrib-clean');
59
71
  grunt.loadNpmTasks('grunt-jsdoc');
72
+ grunt.loadNpmTasks('grunt-run');
60
73
  grunt.registerTask('build', [
61
74
  'clean:build',
62
75
  'copy',
package/README.md CHANGED
@@ -87,7 +87,7 @@ cp -r out/* /var/www/html/flashphoner-sfu-test
87
87
 
88
88
  [SFU functions description](https://docs.flashphoner.com/display/WCS52EN/SFU+functions+with+Simulcast)
89
89
  [SFU SDK documentation](https://docs.flashphoner.com/display/SS1E/SFU+SDK+1.0+-+EN)
90
- [SFU client example description](https://docs.flashphoner.com/display/SS1E/SFU+client)
90
+ [SFU examples description](https://docs.flashphoner.com/display/SS1E/SFU+SDK+Examples)
91
91
  [API documentation](http://flashphoner.com/docs/api/WCS5/client/sfu-sdk/latest)
92
92
 
93
93
  ## Known issues
@@ -1,4 +1,4 @@
1
- SFU SDK - 1.0.1-35
1
+ SFU SDK - 1.0.40
2
2
 
3
3
  [Download builds](https://docs.flashphoner.com/display/SS1E/Release+notes)
4
4
 
package/package.json CHANGED
@@ -1,7 +1,10 @@
1
1
  {
2
2
  "name": "@flashphoner/sfusdk",
3
3
  "description": "Official Flashphoner WebCallServer SFU SDK package",
4
- "version": "1.0.1-35",
4
+ "version": "1.0.40",
5
+ "scripts": {
6
+ "test": "jest --runInBand"
7
+ },
5
8
  "dependencies": {
6
9
  "bootstrap": "^4.6.0",
7
10
  "datatables.net": "^1.10.24",
@@ -10,15 +13,17 @@
10
13
  "jquery-ui": "^1.12.1",
11
14
  "popper.js": "^1.16.1",
12
15
  "sdp-transform": "^2.14.0",
13
- "webrtc-adapter": "^7.2.6",
14
- "uuid": "^8.3.0"
16
+ "uuid": "^8.3.0",
17
+ "webrtc-adapter": "^7.2.6"
15
18
  },
16
19
  "devDependencies": {
17
20
  "grunt": "^1.0.1",
18
21
  "grunt-browserify": "^5.0.0",
19
22
  "grunt-contrib-clean": "^1.0.0",
20
23
  "grunt-contrib-copy": "^1.0.0",
21
- "grunt-jsdoc": "^2.4.0"
24
+ "grunt-jsdoc": "^2.4.0",
25
+ "grunt-run": "^0.8.1",
26
+ "jest": "^27.4.7"
22
27
  },
23
28
  "keywords": [
24
29
  "Flashphoner",
@@ -26,5 +31,8 @@
26
31
  "SFU SDK"
27
32
  ],
28
33
  "author": "Flashphoner",
29
- "license": "MIT"
34
+ "license": "MIT",
35
+ "jest" : {
36
+ "testEnvironment": "jsdom"
37
+ }
30
38
  }
@@ -0,0 +1,81 @@
1
+ const getRoomConfig = function(config) {
2
+ let roomConfig = {
3
+ url: config.url || "ws://127.0.0.1:8080",
4
+ roomName: config.name || "ROOM1",
5
+ pin: config.pin || "1234",
6
+ nickname: config.nickName || "User1"
7
+ };
8
+ return roomConfig;
9
+ }
10
+
11
+ const getVideoStreams = async function(config) {
12
+ let streams = [];
13
+ if (config.media && config.media.video && config.media.video.tracks) {
14
+ streams = await getStreams(config.media.video.tracks);
15
+ }
16
+ return streams;
17
+ }
18
+
19
+ const getAudioStreams = async function(config) {
20
+ let streams = [];
21
+ if (config.media && config.media.audio && config.media.audio.tracks) {
22
+ streams = await getStreams(config.media.audio.tracks);
23
+ }
24
+ return streams;
25
+ }
26
+
27
+ const getStreams = async function(tracks) {
28
+ let streams = [];
29
+ for (let track of tracks) {
30
+ let stream = await getMedia(track);
31
+ if (stream) {
32
+ streams.push({
33
+ stream: stream,
34
+ encodings: track.encodings,
35
+ source: track.source
36
+ });
37
+ }
38
+ }
39
+ return streams;
40
+ }
41
+
42
+ const getMedia = async function(track) {
43
+ //convert to constraints
44
+ let screen = false;
45
+ const constraints= {};
46
+ if (track.source === "mic") {
47
+ //audio
48
+ constraints.audio = {};
49
+ if (track.constraints) {
50
+ constraints.audio = track.constraints;
51
+ }
52
+ if (track.channels && track.channels === 2) {
53
+ constraints.audio.echoCancellation = false;
54
+ constraints.audio.googEchoCancellation = false;
55
+ }
56
+ } else if (track.source === "camera") {
57
+ constraints.video = {};
58
+ if (track.constraints) {
59
+ constraints.video = track.constraints;
60
+ }
61
+ constraints.video.width = track.width;
62
+ constraints.video.height = track.height;
63
+ } else if (track.source === "screen") {
64
+ constraints.video = {};
65
+ if (track.constraints) {
66
+ constraints.video = track.constraints;
67
+ }
68
+ constraints.video.width = track.width;
69
+ constraints.video.height = track.height;
70
+ screen = true;
71
+ }
72
+
73
+ //get access to a/v
74
+ let stream;
75
+ if (screen) {
76
+ stream = await navigator.mediaDevices.getDisplayMedia(constraints);
77
+ } else {
78
+ stream = await navigator.mediaDevices.getUserMedia(constraints);
79
+ }
80
+ return stream;
81
+ }
@@ -0,0 +1,484 @@
1
+ const initLocalDisplay = function(localDisplayElement){
2
+ const localDisplayDiv = localDisplayElement;
3
+ const localDisplays = {};
4
+
5
+ const removeLocalDisplay = function(id) {
6
+ let localDisplay = document.getElementById(localDisplays[id].id);
7
+ let video = localDisplay.getElementsByTagName("video");
8
+ if (video && video[0]) {
9
+ for (const [key, vTrack] of Object.entries(video[0].srcObject.getTracks())) {
10
+ vTrack.stop();
11
+ }
12
+ }
13
+ delete localDisplays[id];
14
+ localDisplay.remove();
15
+ }
16
+
17
+ const getAudioContainer = function() {
18
+ for (const [key, value] of Object.entries(localDisplays)) {
19
+ let video = value.getElementsByTagName("video");
20
+ if (video && video[0]) {
21
+ let audioStateButton = value.getElementsByTagName("button");
22
+ let audioTracks = video[0].srcObject.getAudioTracks();
23
+ if (!audioTracks || audioTracks.length === 0) {
24
+ return {
25
+ id: value.id,
26
+ video: video[0],
27
+ audioStateDisplay: audioStateButton[0]
28
+ }
29
+ }
30
+ }
31
+ }
32
+ };
33
+
34
+ const add = function(id, name, stream) {
35
+ if (stream.getAudioTracks().length > 0) {
36
+ let videoElement = getAudioContainer();
37
+ if (videoElement) {
38
+ let track = stream.getAudioTracks()[0];
39
+ videoElement.video.srcObject.addTrack(track);
40
+ videoElement.audioStateDisplay.innerHTML = audioStateText(stream);
41
+ track.addEventListener("ended", function() {
42
+ videoElement.video.srcObject.removeTrack(track);
43
+ videoElement.audioStateDisplay.innerHTML = "No audio";
44
+ //check video element has no tracks left
45
+ for (const [key, vTrack] of Object.entries(videoElement.video.srcObject.getTracks())) {
46
+ if (vTrack.readyState !== "ended") {
47
+ return;
48
+ }
49
+ }
50
+ removeLocalDisplay(videoElement.id);
51
+ });
52
+ return;
53
+ }
54
+ }
55
+
56
+ const coreDisplay = document.createElement('div');
57
+ coreDisplay.setAttribute("class","text-center");
58
+ coreDisplay.setAttribute("style","width: auto; height: auto;");
59
+ coreDisplay.id = stream.id;
60
+ const streamNameDisplay = document.createElement("div");
61
+ streamNameDisplay.innerHTML = "Name: " + name;
62
+ streamNameDisplay.setAttribute("class","text-center");
63
+ streamNameDisplay.setAttribute("style","width: auto; height: auto;");
64
+ coreDisplay.appendChild(streamNameDisplay);
65
+
66
+ const audioStateDisplay = document.createElement("button");
67
+ audioStateDisplay.innerHTML = audioStateText(stream);
68
+ audioStateDisplay.addEventListener('click', function(){
69
+ if (stream.getAudioTracks().length > 0) {
70
+ stream.getAudioTracks()[0].enabled = !(stream.getAudioTracks()[0].enabled);
71
+ audioStateDisplay.innerHTML = audioStateText(stream);
72
+ }
73
+ });
74
+ coreDisplay.appendChild(audioStateDisplay);
75
+
76
+ const streamDisplay = document.createElement('div');
77
+ streamDisplay.id = "stream-" + id;
78
+ streamDisplay.setAttribute("class","text-center");
79
+ streamDisplay.setAttribute("style","width: auto; height: auto;");
80
+ coreDisplay.appendChild(streamDisplay);
81
+ const video = document.createElement("video");
82
+ video.muted = true;
83
+ if(Browser().isSafariWebRTC()) {
84
+ video.setAttribute("playsinline", "");
85
+ video.setAttribute("webkit-playsinline", "");
86
+ }
87
+ streamDisplay.appendChild(video);
88
+ video.srcObject = stream;
89
+ video.onloadedmetadata = function (e) {
90
+ video.play();
91
+ };
92
+ stream.getTracks().forEach(function(track){
93
+ track.addEventListener("ended", function() {
94
+ video.srcObject.removeTrack(track);
95
+ //check video element has no tracks left
96
+ for (const [key, vTrack] of Object.entries(video.srcObject.getTracks())) {
97
+ if (vTrack.readyState !== "ended") {
98
+ return;
99
+ }
100
+ }
101
+ removeLocalDisplay(id);
102
+ });
103
+ });
104
+ video.addEventListener('resize', function (event) {
105
+ streamNameDisplay.innerHTML = "Name: " + name + "<br/>Max.resolution: " + video.videoWidth + "x" + video.videoHeight;
106
+ resizeVideo(event.target);
107
+ });
108
+ localDisplays[id] = coreDisplay;
109
+ localDisplayDiv.appendChild(coreDisplay);
110
+ return coreDisplay;
111
+ }
112
+
113
+ const stop = function () {
114
+ for (const [key, value] of Object.entries(localDisplays)) {
115
+ removeLocalDisplay(value.id);
116
+ }
117
+ }
118
+
119
+ const audioStateText = function (stream) {
120
+ if (stream.getAudioTracks().length > 0) {
121
+ if (stream.getAudioTracks()[0].enabled) {
122
+ return "Mute";
123
+ } else {
124
+ return "Unmute";
125
+ }
126
+ }
127
+ return "No audio";
128
+ }
129
+
130
+ return {
131
+ add: add,
132
+ stop: stop
133
+ }
134
+ }
135
+
136
+ const initRemoteDisplay = function(mainDiv, room, peerConnection) {
137
+ const constants = SFU.constants;
138
+ const remoteParticipants = {};
139
+ room.on(constants.SFU_ROOM_EVENT.ADD_TRACKS, function(e) {
140
+ console.log("Received ADD_TRACKS");
141
+ let participant = remoteParticipants[e.info.nickName];
142
+ if (!participant) {
143
+ participant = {};
144
+ participant.nickName = e.info.nickName;
145
+ participant.tracks = [];
146
+ participant.displays = [];
147
+ remoteParticipants[participant.nickName] = participant;
148
+ }
149
+ participant.tracks.push.apply(participant.tracks, e.info.info);
150
+ for (const pTrack of e.info.info) {
151
+ let createDisplay = true;
152
+ for (let i = 0; i < participant.displays.length; i++) {
153
+ let display = participant.displays[i];
154
+ if (pTrack.type === "VIDEO") {
155
+ if (display.hasVideo()) {
156
+ continue;
157
+ }
158
+ display.videoMid = pTrack.mid;
159
+ display.setTrackInfo(pTrack);
160
+ createDisplay = false;
161
+ break;
162
+ } else if (pTrack.type === "AUDIO") {
163
+ if (display.hasAudio()) {
164
+ continue;
165
+ }
166
+ display.audioMid = pTrack.mid;
167
+ createDisplay = false;
168
+ break;
169
+ }
170
+ }
171
+ if (!createDisplay) {
172
+ continue;
173
+ }
174
+ let display = createRemoteDisplay(participant.nickName, participant.nickName, mainDiv);
175
+ participant.displays.push(display);
176
+ if (pTrack.type === "VIDEO") {
177
+ display.videoMid = pTrack.mid;
178
+ display.setTrackInfo(pTrack);
179
+ } else if (pTrack.type === "AUDIO") {
180
+ display.audioMid = pTrack.mid;
181
+ }
182
+ }
183
+ }).on(constants.SFU_ROOM_EVENT.REMOVE_TRACKS, function(e) {
184
+ console.log("Received REMOVE_TRACKS");
185
+ const participant = remoteParticipants[e.info.nickName];
186
+ if (!participant) {
187
+ return;
188
+ }
189
+ for (const rTrack of e.info.info) {
190
+ for (let i = 0; i < participant.tracks.length; i++) {
191
+ if (rTrack.mid === participant.tracks[i].mid) {
192
+ participant.tracks.splice(i, 1);
193
+ break;
194
+ }
195
+ }
196
+ for (let i = 0; i < participant.displays.length; i++) {
197
+ let found = false;
198
+ const display = participant.displays[i];
199
+ if (display.audioMid === rTrack.mid) {
200
+ display.setAudio(null);
201
+ found = true;
202
+ } else if (display.videoMid === rTrack.mid) {
203
+ display.setVideo(null);
204
+ found = true;
205
+ }
206
+ if (found) {
207
+ if (!display.hasAudio() && !display.hasVideo()) {
208
+ display.dispose();
209
+ participant.displays.splice(i, 1);
210
+ }
211
+ break;
212
+ }
213
+ }
214
+ }
215
+ }).on(constants.SFU_ROOM_EVENT.LEFT, function(e) {
216
+ console.log("Received LEFT");
217
+ let participant = remoteParticipants[e.name];
218
+ if (!participant) {
219
+ return;
220
+ }
221
+ participant.displays.forEach(function(display){
222
+ display.dispose();
223
+ })
224
+ delete remoteParticipants[e.name];
225
+ }).on(constants.SFU_ROOM_EVENT.TRACK_QUALITY_STATE, function(e){
226
+ console.log("Received track quality state");
227
+ const participant = remoteParticipants[e.info.nickName];
228
+ if (!participant) {
229
+ return;
230
+ }
231
+
232
+ for (const rTrack of e.info.tracks) {
233
+ const mid = rTrack.mid;
234
+ for (let i = 0; i < participant.displays.length; i++) {
235
+ const display = participant.displays[i];
236
+ if (display.videoMid === mid) {
237
+ display.updateQualityInfo(rTrack.quality);
238
+ break;
239
+ }
240
+ }
241
+ }
242
+ });
243
+
244
+ const createRemoteDisplay = function(id, name, mainDiv) {
245
+ const cell = document.createElement("div");
246
+ cell.setAttribute("class", "text-center");
247
+ cell.id = id;
248
+ mainDiv.appendChild(cell);
249
+ const streamNameDisplay = document.createElement("div");
250
+ streamNameDisplay.innerHTML = "Published by: " + name;
251
+ streamNameDisplay.setAttribute("style","width:auto; height:auto;");
252
+ streamNameDisplay.setAttribute("class","text-center");
253
+ cell.appendChild(streamNameDisplay);
254
+ const qualityDisplay = document.createElement("div");
255
+ qualityDisplay.setAttribute("style","width:auto; height:auto;");
256
+ qualityDisplay.setAttribute("class","text-center");
257
+ cell.appendChild(qualityDisplay);
258
+
259
+ let qualityDivs = [];
260
+
261
+ const rootDisplay = document.createElement("div");
262
+ rootDisplay.setAttribute("style","width:auto; height:auto;");
263
+ rootDisplay.setAttribute("class","text-center");
264
+ cell.appendChild(rootDisplay);
265
+ const streamDisplay = document.createElement("div");
266
+ streamDisplay.setAttribute("style","width:auto; height:auto;");
267
+ streamDisplay.setAttribute("class","text-center");
268
+ rootDisplay.appendChild(streamDisplay);
269
+
270
+ let audio = null;
271
+ let video = null;
272
+ return {
273
+ dispose: function() {
274
+ cell.remove();
275
+ },
276
+ hide: function(value) {
277
+ if (value) {
278
+ cell.style.display = "none";
279
+ } else {
280
+ cell.style.display = "block";
281
+ }
282
+ },
283
+ setAudio: function(stream) {
284
+ if (audio) {
285
+ audio.remove();
286
+ }
287
+ if (!stream) {
288
+ audio = null;
289
+ this.audioMid = undefined;
290
+ return;
291
+ }
292
+ audio = document.createElement("audio");
293
+ audio.controls = "controls";
294
+ audio.muted = true;
295
+ audio.autoplay = true;
296
+ cell.appendChild(audio);
297
+ audio.srcObject = stream;
298
+ audio.onloadedmetadata = function (e) {
299
+ audio.play().then(function() {
300
+ audio.muted = false;
301
+ });
302
+ };
303
+ },
304
+ hasAudio: function() {
305
+ return audio !== null || this.audioMid !== undefined;
306
+ },
307
+ setVideo: function(stream) {
308
+ if (video) {
309
+ video.remove();
310
+ }
311
+
312
+ if (stream == null) {
313
+ video = null;
314
+ this.videoMid = undefined;
315
+ qualityDivs.forEach(function(div) {
316
+ div.remove();
317
+ });
318
+ qualityDivs = [];
319
+ return;
320
+ }
321
+ video = document.createElement("video");
322
+ video.muted = true;
323
+ if(Browser().isSafariWebRTC()) {
324
+ video.setAttribute("playsinline", "");
325
+ video.setAttribute("webkit-playsinline", "");
326
+ }
327
+ streamDisplay.appendChild(video);
328
+ video.srcObject = stream;
329
+ video.onloadedmetadata = function (e) {
330
+ video.play().then(function() {
331
+ video.muted = false;
332
+ });
333
+ };
334
+ video.addEventListener("resize", function (event) {
335
+ streamNameDisplay.innerHTML = "Published by: " + name + "<br/>Current resolution: " + video.videoWidth + "x" + video.videoHeight;
336
+ resizeVideo(event.target);
337
+ });
338
+ },
339
+ setTrackInfo: function(trackInfo) {
340
+ if (trackInfo && trackInfo.quality) {
341
+ for (let i = 0; i < trackInfo.quality.length; i++) {
342
+ const qualityDiv = document.createElement("button");
343
+ qualityDivs.push(qualityDiv);
344
+ qualityDiv.innerText = trackInfo.quality[i];
345
+ qualityDiv.setAttribute("style", "display:inline-block; border: solid; border-width: 1px");
346
+ qualityDiv.style.color = "red";
347
+ qualityDiv.addEventListener('click', function(){
348
+ console.log("Clicked on quality " + trackInfo.quality[i] + " trackId " + trackInfo.id);
349
+ if (qualityDiv.style.color === "red") {
350
+ return;
351
+ }
352
+ for (let c = 0; c < qualityDivs.length; c++) {
353
+ if (qualityDivs[c].style.color !== "red") {
354
+ qualityDivs[c].style.color = "gray";
355
+ }
356
+ }
357
+ qualityDiv.style.color = "blue";
358
+ room.changeQuality(trackInfo.id, trackInfo.quality[i]);
359
+ });
360
+ qualityDisplay.appendChild(qualityDiv);
361
+ }
362
+ }
363
+ },
364
+ updateQualityInfo: function(videoQuality) {
365
+ for (const qualityInfo of videoQuality) {
366
+ for (const qualityDiv of qualityDivs) {
367
+ if (qualityDiv.innerText === qualityInfo.quality){
368
+ if (qualityInfo.available === true) {
369
+ qualityDiv.style.color = "gray";
370
+ } else {
371
+ qualityDiv.style.color = "red";
372
+ }
373
+ break;
374
+ }
375
+ }
376
+ }
377
+ },
378
+ hasVideo: function() {
379
+ return video !== null || this.videoMid !== undefined;
380
+ },
381
+ audioMid: undefined,
382
+ videoMid: undefined
383
+ };
384
+ }
385
+
386
+ const stop = function() {
387
+ for (const [nickName, participant] of Object.entries(remoteParticipants)) {
388
+ participant.displays.forEach(function(display){
389
+ display.dispose();
390
+ });
391
+ delete remoteParticipants[nickName];
392
+ }
393
+ }
394
+
395
+ peerConnection.ontrack = ({transceiver}) => {
396
+ let rParticipant;
397
+ console.log("Attach remote track " + transceiver.receiver.track.id + " kind " + transceiver.receiver.track.kind + " mid " + transceiver.mid);
398
+ for (const [nickName, participant] of Object.entries(remoteParticipants)) {
399
+ for (const pTrack of participant.tracks) {
400
+ console.log("Participant " + participant.nickName + " track " + pTrack.id + " mid " + pTrack.mid);
401
+ if (pTrack.mid === transceiver.mid) {
402
+ rParticipant = participant;
403
+ break;
404
+ }
405
+ }
406
+ if (rParticipant) {
407
+ break;
408
+ }
409
+ }
410
+ if (rParticipant) {
411
+ for (const display of rParticipant.displays) {
412
+ if (transceiver.receiver.track.kind === "video") {
413
+ if (display.videoMid === transceiver.mid) {
414
+ let stream = new MediaStream();
415
+ stream.addTrack(transceiver.receiver.track);
416
+ display.setVideo(stream);
417
+ break;
418
+ }
419
+ } else if (transceiver.receiver.track.kind === "audio") {
420
+ if (display.audioMid === transceiver.mid) {
421
+ let stream = new MediaStream();
422
+ stream.addTrack(transceiver.receiver.track);
423
+ display.setAudio(stream);
424
+ break;
425
+ }
426
+ }
427
+ }
428
+ } else {
429
+ console.warn("Failed to find participant for track " + transceiver.receiver.track.id);
430
+ }
431
+ }
432
+
433
+ return {
434
+ stop: stop
435
+ }
436
+ }
437
+
438
+ const resizeVideo = function(video, width, height) {
439
+ if (!video.parentNode) {
440
+ return;
441
+ }
442
+ if (video instanceof HTMLCanvasElement) {
443
+ video.videoWidth = video.width;
444
+ video.videoHeight = video.height;
445
+ }
446
+ var display = video.parentNode;
447
+ var parentSize = {
448
+ w: display.parentNode.clientWidth,
449
+ h: display.parentNode.clientHeight
450
+ };
451
+ var newSize;
452
+ if (width && height) {
453
+ newSize = downScaleToFitSize(width, height, parentSize.w, parentSize.h);
454
+ } else {
455
+ newSize = downScaleToFitSize(video.videoWidth, video.videoHeight, parentSize.w, parentSize.h);
456
+ }
457
+ display.style.width = newSize.w + "px";
458
+ display.style.height = newSize.h + "px";
459
+
460
+ //vertical align
461
+ var margin = 0;
462
+ if (parentSize.h - newSize.h > 1) {
463
+ margin = Math.floor((parentSize.h - newSize.h) / 2);
464
+ }
465
+ display.style.margin = margin + "px auto";
466
+ console.log("Resize from " + video.videoWidth + "x" + video.videoHeight + " to " + display.offsetWidth + "x" + display.offsetHeight);
467
+ }
468
+
469
+ const downScaleToFitSize = function(videoWidth, videoHeight, dstWidth, dstHeight) {
470
+ var newWidth, newHeight;
471
+ var videoRatio = videoWidth / videoHeight;
472
+ var dstRatio = dstWidth / dstHeight;
473
+ if (dstRatio > videoRatio) {
474
+ newHeight = dstHeight;
475
+ newWidth = Math.floor(videoRatio * dstHeight);
476
+ } else {
477
+ newWidth = dstWidth;
478
+ newHeight = Math.floor(dstWidth / videoRatio);
479
+ }
480
+ return {
481
+ w: newWidth,
482
+ h: newHeight
483
+ };
484
+ }