ソースを参照

Add video background blur

j8
Cristian Florin Ghita 5年前
コミット
3b750ddd5a

+ 2
- 0
Makefile ファイルの表示

@@ -45,6 +45,8 @@ deploy-appbundle:
45 45
 		$(OUTPUT_DIR)/analytics-ga.js \
46 46
 		$(BUILD_DIR)/analytics-ga.min.js \
47 47
 		$(BUILD_DIR)/analytics-ga.min.map \
48
+		$(BUILD_DIR)/video-blur-effect.min.js \
49
+		$(BUILD_DIR)/video-blur-effect.min.map \
48 50
 		$(DEPLOY_DIR)
49 51
 
50 52
 deploy-lib-jitsi-meet:

+ 67
- 36
conference.js ファイルの表示

@@ -105,7 +105,10 @@ import {
105 105
     trackAdded,
106 106
     trackRemoved
107 107
 } from './react/features/base/tracks';
108
-import { getJitsiMeetGlobalNS } from './react/features/base/util';
108
+import {
109
+    getJitsiMeetGlobalNS,
110
+    loadScript
111
+} from './react/features/base/util';
109 112
 import { addMessage } from './react/features/chat';
110 113
 import { showDesktopPicker } from './react/features/desktop-picker';
111 114
 import { appendSuffix } from './react/features/display-name';
@@ -559,48 +562,74 @@ export default {
559 562
             // Resolve with no tracks
560 563
             tryCreateLocalTracks = Promise.resolve([]);
561 564
         } else {
562
-            tryCreateLocalTracks = createLocalTracksF(
563
-                { devices: initialDevices }, true)
564
-                .catch(err => {
565
-                    if (requestedAudio && requestedVideo) {
566 565
 
567
-                        // Try audio only...
568
-                        audioAndVideoError = err;
566
+            const loadEffectsPromise = options.startWithBlurEnabled
567
+                ? loadScript('libs/video-blur-effect.min.js')
568
+                        .then(() =>
569
+                            getJitsiMeetGlobalNS().effects.createBlurEffect()
570
+                                .then(blurEffectInstance =>
571
+                                    Promise.resolve([ blurEffectInstance ])
572
+                                )
573
+                                .catch(error => {
574
+                                    logger.log('Failed to create JitsiStreamBlurEffect!', error);
575
+
576
+                                    return Promise.resolve([]);
577
+                                })
578
+                        )
579
+                        .catch(error => {
580
+                            logger.error('loadScript failed with error: ', error);
581
+
582
+                            return Promise.resolve([]);
583
+                        })
584
+                : Promise.resolve([]);
585
+
586
+            tryCreateLocalTracks = loadEffectsPromise.then(trackEffects =>
587
+                createLocalTracksF(
588
+                    {
589
+                        devices: initialDevices,
590
+                        effects: trackEffects
591
+                    }, true)
592
+                    .catch(err => {
593
+                        if (requestedAudio && requestedVideo) {
594
+
595
+                            // Try audio only...
596
+                            audioAndVideoError = err;
569 597
 
570
-                        return (
571
-                            createLocalTracksF({ devices: [ 'audio' ] }, true));
572
-                    } else if (requestedAudio && !requestedVideo) {
598
+                            return (
599
+                                createLocalTracksF({ devices: [ 'audio' ] }, true));
600
+                        } else if (requestedAudio && !requestedVideo) {
601
+                            audioOnlyError = err;
602
+
603
+                            return [];
604
+                        } else if (requestedVideo && !requestedAudio) {
605
+                            videoOnlyError = err;
606
+
607
+                            return [];
608
+                        }
609
+                        logger.error('Should never happen');
610
+                    })
611
+                    .catch(err => {
612
+                        // Log this just in case...
613
+                        if (!requestedAudio) {
614
+                            logger.error('The impossible just happened', err);
615
+                        }
573 616
                         audioOnlyError = err;
574 617
 
575
-                        return [];
576
-                    } else if (requestedVideo && !requestedAudio) {
618
+                        // Try video only...
619
+                        return requestedVideo
620
+                            ? createLocalTracksF({ devices: [ 'video' ] }, true)
621
+                            : [];
622
+                    })
623
+                    .catch(err => {
624
+                        // Log this just in case...
625
+                        if (!requestedVideo) {
626
+                            logger.error('The impossible just happened', err);
627
+                        }
577 628
                         videoOnlyError = err;
578 629
 
579 630
                         return [];
580
-                    }
581
-                    logger.error('Should never happen');
582
-                })
583
-                .catch(err => {
584
-                    // Log this just in case...
585
-                    if (!requestedAudio) {
586
-                        logger.error('The impossible just happened', err);
587
-                    }
588
-                    audioOnlyError = err;
589
-
590
-                    // Try video only...
591
-                    return requestedVideo
592
-                        ? createLocalTracksF({ devices: [ 'video' ] }, true)
593
-                        : [];
594
-                })
595
-                .catch(err => {
596
-                    // Log this just in case...
597
-                    if (!requestedVideo) {
598
-                        logger.error('The impossible just happened', err);
599
-                    }
600
-                    videoOnlyError = err;
601
-
602
-                    return [];
603
-                });
631
+                    })
632
+            );
604 633
         }
605 634
 
606 635
         // Hide the permissions prompt/overlay as soon as the tracks are
@@ -649,6 +678,7 @@ export default {
649 678
      */
650 679
     init(options) {
651 680
         this.roomName = options.roomName;
681
+        const videoBlurEffectEnabled = APP.store.getState()['features/blur'].blurEnabled;
652 682
 
653 683
         return (
654 684
 
@@ -662,6 +692,7 @@ export default {
662 692
                     'initial device list initialization failed', error))
663 693
                 .then(() => this.createInitialLocalTracksAndConnect(
664 694
                 options.roomName, {
695
+                    startWithBlurEnabled: videoBlurEffectEnabled,
665 696
                     startAudioOnly: config.startAudioOnly,
666 697
                     startScreenSharing: config.startScreenSharing,
667 698
                     startWithAudioMuted: config.startWithAudioMuted || config.startSilent,

+ 4
- 0
css/_font.scss ファイルの表示

@@ -220,3 +220,7 @@
220 220
 .icon-visibility-off:before {
221 221
   content: "\e924";
222 222
 }
223
+.icon-blur-background:before {
224
+  content: "\e901";
225
+  color: #a4b8d1;
226
+}

バイナリ
fonts/jitsi.eot ファイルの表示


+ 1
- 4
fonts/jitsi.svg ファイルの表示

@@ -28,16 +28,13 @@
28 28
 <glyph unicode="&#xe8b3;" glyph-name="restore" d="M512 682h64v-180l150-90-32-52-182 110v212zM554 896c212 0 384-172 384-384s-172-384-384-384c-106 0-200 42-270 112l60 62c54-54 128-88 210-88 166 0 300 132 300 298s-134 298-300 298-298-132-298-298h128l-172-172-4 6-166 166h128c0 212 172 384 384 384z" />
29 29
 <glyph unicode="&#xe8b6;" glyph-name="search" d="M406 426c106 0 192 86 192 192s-86 192-192 192-192-86-192-192 86-192 192-192zM662 426l212-212-64-64-212 212v34l-12 12c-48-42-112-66-180-66-154 0-278 122-278 276s124 278 278 278 276-124 276-278c0-68-24-132-66-180l12-12h34z" />
30 30
 <glyph unicode="&#xe900;" glyph-name="AUD" d="M512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512c282.77 0 512-229.23 512-512s-229.23-512-512-512zM308.25 387.3h57.225l-87.675 252.525h-62.125l-87.675-252.525h53.025l19.425 60.2h88.725l19.075-60.2zM461.9 639.825h-52.85v-165.375c0-56 41.125-93.625 105.7-93.625 64.75 0 105.875 37.625 105.875 93.625v165.375h-52.85v-159.95c0-31.85-19.075-52.15-53.025-52.15-33.775 0-52.85 20.3-52.85 52.15v159.95zM682.225 640v-252.7h99.4c75.6 0 118.475 46.025 118.475 128.1 0 79.1-43.4 124.6-118.475 124.6h-99.4zM735.075 594.85v-162.4h38.15c46.725 0 72.975 28.7 72.975 82.075 0 51.1-27.125 80.325-72.975 80.325h-38.15zM243.5 587.325l-31.675-99.050h66.15l-31.325 99.050h-3.15z" />
31
-<glyph unicode="&#xe901;" glyph-name="signal_cellular_0" d="M938 938v-852h-852zM854 732l-562-562h562v562z" />
32
-<glyph unicode="&#xe902;" glyph-name="signal_cellular_1" d="M86 86l852 852v-256h-170v-596h-682zM854 86v84h84v-84h-84zM854 256v342h84v-342h-84z" />
31
+<glyph unicode="&#xe901;" glyph-name="blur-background" d="M469.333 640c0-47.128-38.205-85.333-85.333-85.333s-85.333 38.205-85.333 85.333c0 47.128 38.205 85.333 85.333 85.333s85.333-38.205 85.333-85.333zM725.333 640c0-47.128-38.205-85.333-85.333-85.333s-85.333 38.205-85.333 85.333c0 47.128 38.205 85.333 85.333 85.333s85.333-38.205 85.333-85.333zM469.333 384c0-47.128-38.205-85.333-85.333-85.333s-85.333 38.205-85.333 85.333c0 47.128 38.205 85.333 85.333 85.333s85.333-38.205 85.333-85.333zM426.667 170.667c0-23.564-19.103-42.667-42.667-42.667s-42.667 19.103-42.667 42.667c0 23.564 19.103 42.667 42.667 42.667s42.667-19.103 42.667-42.667zM682.667 170.667c0-23.564-19.103-42.667-42.667-42.667s-42.667 19.103-42.667 42.667c0 23.564 19.103 42.667 42.667 42.667s42.667-19.103 42.667-42.667zM213.333 384c0-23.564-19.103-42.667-42.667-42.667s-42.667 19.103-42.667 42.667c0 23.564 19.103 42.667 42.667 42.667s42.667-19.103 42.667-42.667zM213.333 640c0-23.564-19.103-42.667-42.667-42.667s-42.667 19.103-42.667 42.667c0 23.564 19.103 42.667 42.667 42.667s42.667-19.103 42.667-42.667zM896 384c0-23.564-19.103-42.667-42.667-42.667s-42.667 19.103-42.667 42.667c0 23.564 19.103 42.667 42.667 42.667s42.667-19.103 42.667-42.667zM896 640c0-23.564-19.103-42.667-42.667-42.667s-42.667 19.103-42.667 42.667c0 23.564 19.103 42.667 42.667 42.667s42.667-19.103 42.667-42.667zM426.667 853.333c0-23.564-19.103-42.667-42.667-42.667s-42.667 19.103-42.667 42.667c0 23.564 19.103 42.667 42.667 42.667s42.667-19.103 42.667-42.667zM682.667 853.333c0-23.564-19.103-42.667-42.667-42.667s-42.667 19.103-42.667 42.667c0 23.564 19.103 42.667 42.667 42.667s42.667-19.103 42.667-42.667zM725.333 384c0-47.128-38.205-85.333-85.333-85.333s-85.333 38.205-85.333 85.333c0 47.128 38.205 85.333 85.333 85.333s85.333-38.205 85.333-85.333z" />
33 32
 <glyph unicode="&#xe903;" glyph-name="mic-camera-combined" d="M756.704 628.138l267.296 202.213v-635.075l-267.296 202.213v-191.923c0-12.085-11.296-21.863-25.216-21.863h-706.272c-13.92 0-25.216 9.777-25.216 21.863v612.25c0 12.085 11.296 21.863 25.216 21.863h706.272c13.92 0 25.216-9.777 25.216-21.863v-189.679zM371.338 376.228c47.817 0 86.529 40.232 86.529 89.811v184.835c0 49.651-38.713 89.883-86.529 89.883-47.788 0-86.515-40.232-86.515-89.883v-184.835c0-49.579 38.756-89.811 86.515-89.811v0zM356.754 314.070v-32.78h33.718v33.412c73.858 9.606 131.235 73.73 131.235 151.351v88.232h-30.636v-88.232c0-67.57-53.696-122.534-119.734-122.534-66.024 0-119.691 54.964-119.691 122.534v88.232h-30.636v-88.232c0-79.215 59.674-144.502 135.744-151.969v-0.014z" />
34 33
 <glyph unicode="&#xe904;" glyph-name="kick" d="M512 810l284-426h-568zM214 298h596v-84h-596v84z" />
35 34
 <glyph unicode="&#xe905;" glyph-name="hangup" d="M512 640c-68 0-134-10-196-30v-132c0-16-10-34-24-40-42-20-80-46-114-78-8-8-18-12-30-12s-22 4-30 12l-106 106c-8 8-12 18-12 30s4 22 12 30c130 124 306 200 500 200s370-76 500-200c8-8 12-18 12-30s-4-22-12-30l-106-106c-8-8-18-12-30-12s-22 4-30 12c-34 32-72 58-114 78-14 6-24 20-24 38v132c-62 20-128 32-196 32z" />
36 35
 <glyph unicode="&#xe906;" glyph-name="chat" d="M854 342v512h-684v-598l86 86h598zM854 938c46 0 84-38 84-84v-512c0-46-38-86-84-86h-598l-170-170v768c0 46 38 84 84 84h684z" />
37
-<glyph unicode="&#xe907;" glyph-name="signal_cellular_2" d="M86 86l852 852v-852h-852z" />
38 36
 <glyph unicode="&#xe908;" glyph-name="share-doc" d="M554 640h236l-236 234v-234zM682 426v86h-340v-86h340zM682 256v86h-340v-86h340zM598 938l256-256v-512c0-46-40-84-86-84h-512c-46 0-86 38-86 84l2 684c0 46 38 84 84 84h342z" />
39 37
 <glyph unicode="&#xe909;" glyph-name="ninja" d="M330.667 469.333c-0.427 14.933 6.4 29.44 17.92 39.253 32-6.827 61.867-20.053 88.747-39.253 0-29.013-23.893-52.907-53.333-52.907s-52.907 23.467-53.333 52.907zM586.667 469.333c26.88 18.773 56.747 32 88.747 38.827 11.52-9.813 18.347-24.32 17.92-38.827 0-29.867-23.893-53.76-53.333-53.76s-53.333 23.893-53.333 53.76v0zM512 640c-118.187 1.707-234.667-27.733-338.347-85.333l-2.987-42.667c0-52.48 12.373-104.107 35.84-151.040 101.12 15.36 203.093 23.040 305.493 23.040s204.373-7.68 305.493-23.040c23.467 46.933 35.84 98.56 35.84 151.040l-2.987 42.667c-103.68 57.6-220.16 87.040-338.347 85.333zM512 938.667c235.641 0 426.667-191.025 426.667-426.667s-191.025-426.667-426.667-426.667c-235.641 0-426.667 191.025-426.667 426.667s191.025 426.667 426.667 426.667z" />
40
-<glyph unicode="&#xe90a;" glyph-name="enlarge" d="M896 212v600h-768v-600h768zM896 896q34 0 60-26t26-60v-596q0-34-26-60t-60-26h-768q-34 0-60 26t-26 60v596q0 34 26 60t60 26h768zM598 342l-86-108-86 108h172zM256 598v-172l-106 86zM768 598l106-86-106-86v172zM512 790l86-108h-172z" />
41 38
 <glyph unicode="&#xe90b;" glyph-name="full-screen" d="M598 810h212v-212h-84v128h-128v84zM726 298v128h84v-212h-212v84h128zM214 598v212h212v-84h-128v-128h-84zM298 426v-128h128v-84h-212v212h84z" />
42 39
 <glyph unicode="&#xe90c;" glyph-name="exit-full-screen" d="M682 682h128v-84h-212v212h84v-128zM598 214v212h212v-84h-128v-128h-84zM342 682v128h84v-212h-212v84h128zM214 342v84h212v-212h-84v128h-128z" />
43 40
 <glyph unicode="&#xe90d;" glyph-name="security" d="M768 170v428h-512v-428h512zM768 682c46 0 86-38 86-84v-428c0-46-40-84-86-84h-512c-46 0-86 38-86 84v428c0 46 40 84 86 84h388v86c0 72-60 132-132 132s-132-60-132-132h-82c0 118 96 214 214 214s214-96 214-214v-86h42zM512 298c-46 0-86 40-86 86s40 86 86 86 86-40 86-86-40-86-86-86z" />

バイナリ
fonts/jitsi.ttf ファイルの表示


バイナリ
fonts/jitsi.woff ファイルの表示


+ 1
- 1
fonts/selection.json
ファイル差分が大きすぎるため省略します
ファイルの表示


+ 1
- 1
interface_config.js ファイルの表示

@@ -50,7 +50,7 @@ var interfaceConfig = {
50 50
         'fodeviceselection', 'hangup', 'profile', 'info', 'chat', 'recording',
51 51
         'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand',
52 52
         'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts',
53
-        'tileview'
53
+        'tileview', 'videobackgroundblur'
54 54
     ],
55 55
 
56 56
     SETTINGS_SECTIONS: [ 'devices', 'language', 'moderator', 'profile', 'calendar' ],

+ 5
- 2
lang/main.json ファイルの表示

@@ -615,7 +615,8 @@
615 615
             "speakerStats": "Toggle speaker statistics",
616 616
             "tileView": "Toggle tile view",
617 617
             "toggleCamera": "Toggle camera",
618
-            "videomute": "Toggle mute video"
618
+            "videomute": "Toggle mute video",
619
+            "videoblur": "Toggle video blur"
619 620
         },
620 621
         "addPeople": "Add people to your call",
621 622
         "audioonly": "Enable / Disable audio only mode",
@@ -668,7 +669,9 @@
668 669
         "tileViewToggle": "Toggle tile view",
669 670
         "toggleCamera": "Toggle camera",
670 671
         "unableToUnmutePopup": "You cannot un-mute while the shared video is on.",
671
-        "videomute": "Start / Stop camera"
672
+        "videomute": "Start / Stop camera",
673
+        "startvideoblur": "Blur my background",
674
+        "stopvideoblur": "Disable background blur"
672 675
     },
673 676
     "transcribing": {
674 677
         "ccButtonTooltip": "Start / Stop subtitles",

+ 2
- 0
package.json ファイルの表示

@@ -35,6 +35,8 @@
35 35
     "@atlaskit/tooltip": "12.1.13",
36 36
     "@microsoft/microsoft-graph-client": "1.1.0",
37 37
     "@react-native-community/async-storage": "1.3.4",
38
+    "@tensorflow-models/body-pix": "^1.0.1",
39
+    "@tensorflow/tfjs": "^1.1.2",
38 40
     "@webcomponents/url": "0.7.1",
39 41
     "amplitude-js": "4.5.2",
40 42
     "bc-css-flags": "3.0.0",

+ 15
- 0
react/features/analytics/AnalyticsEvents.js ファイルの表示

@@ -467,6 +467,21 @@ export function createRemoteVideoMenuButtonEvent(buttonName, attributes) {
467 467
     };
468 468
 }
469 469
 
470
+/**
471
+ * Creates an event indicating that an action related to video blur
472
+ * occurred (e.g. It was started or stopped).
473
+ *
474
+ * @param {string} action - The action which occurred.
475
+ * @returns {Object} The event in a format suitable for sending via
476
+ * sendAnalytics.
477
+ */
478
+export function createVideoBlurEvent(action) {
479
+    return {
480
+        action,
481
+        actionSubject: 'video.blur'
482
+    };
483
+}
484
+
470 485
 /**
471 486
  * Creates an event indicating that an action related to screen sharing
472 487
  * occurred (e.g. It was started or stopped).

+ 1
- 0
react/features/base/tracks/functions.js ファイルの表示

@@ -71,6 +71,7 @@ export function createLocalTracksF(
71 71
 
72 72
                 // Copy array to avoid mutations inside library.
73 73
                 devices: options.devices.slice(0),
74
+                effects: options.effects,
74 75
                 firefox_fake_device, // eslint-disable-line camelcase
75 76
                 micDeviceId,
76 77
                 resolution

+ 21
- 0
react/features/blur/actionTypes.js ファイルの表示

@@ -0,0 +1,21 @@
1
+// @flow
2
+
3
+/**
4
+ * The type of redux action dispatched which represents that the blur
5
+ * is enabled.
6
+ *
7
+ * {
8
+ *      type: BLUR_ENABLED
9
+ * }
10
+ */
11
+export const BLUR_ENABLED = 'BLUR_ENABLED';
12
+
13
+/**
14
+ * The type of redux action dispatched which represents that the blur
15
+ * is disabled.
16
+ *
17
+ * {
18
+ *      type: BLUR_DISABLED
19
+ * }
20
+ */
21
+export const BLUR_DISABLED = 'BLUR_DISABLED';

+ 69
- 0
react/features/blur/actions.js ファイルの表示

@@ -0,0 +1,69 @@
1
+// @flow
2
+
3
+import { getJitsiMeetGlobalNS } from '../base/util';
4
+import { getLocalVideoTrack } from '../../features/base/tracks';
5
+
6
+import {
7
+    BLUR_DISABLED,
8
+    BLUR_ENABLED
9
+} from './actionTypes';
10
+
11
+const logger = require('jitsi-meet-logger').getLogger(__filename);
12
+
13
+/**
14
+* Signals the local participant is switching between blurred or
15
+* non blurred video.
16
+*
17
+* @param {boolean} enabled - If true enables video blur, false otherwise
18
+*
19
+* @returns {Promise}
20
+*/
21
+export function toggleBlurEffect(enabled: boolean) {
22
+    return function(dispatch: (Object) => Object, getState: () => any) {
23
+        if (getState()['features/blur'].blurEnabled !== enabled) {
24
+            const videoTrack = getLocalVideoTrack(getState()['features/base/tracks']).jitsiTrack;
25
+
26
+            return getJitsiMeetGlobalNS().effects.createBlurEffect()
27
+                .then(blurEffectInstance =>
28
+                    videoTrack.enableEffect(enabled, blurEffectInstance)
29
+                        .then(() => {
30
+                            enabled ? dispatch(blurEnabled()) : dispatch(blurDisabled());
31
+                        })
32
+                        .catch(error => {
33
+                            enabled ? dispatch(blurDisabled()) : dispatch(blurEnabled());
34
+                            logger.log('enableEffect failed with error:', error);
35
+                        })
36
+                )
37
+                .catch(error => {
38
+                    dispatch(blurDisabled());
39
+                    logger.log('createBlurEffect failed with error:', error);
40
+                });
41
+        }
42
+    };
43
+}
44
+
45
+/**
46
+ * Signals the local participant that the blur has been enabled
47
+ *
48
+ * @returns {{
49
+ *      type: BLUR_ENABLED
50
+ * }}
51
+ */
52
+export function blurEnabled() {
53
+    return {
54
+        type: BLUR_ENABLED
55
+    };
56
+}
57
+
58
+/**
59
+ * Signals the local participant that the blur has been disabled
60
+ *
61
+ * @returns {{
62
+ *      type: BLUR_DISABLED
63
+ * }}
64
+ */
65
+export function blurDisabled() {
66
+    return {
67
+        type: BLUR_DISABLED
68
+    };
69
+}

+ 112
- 0
react/features/blur/components/VideoBlurButton.js ファイルの表示

@@ -0,0 +1,112 @@
1
+// @flow
2
+
3
+import { createVideoBlurEvent, sendAnalytics } from '../../analytics';
4
+import { translate } from '../../base/i18n';
5
+import { connect } from '../../base/redux';
6
+import { AbstractButton } from '../../base/toolbox';
7
+import type { AbstractButtonProps } from '../../base/toolbox';
8
+import {
9
+    getJitsiMeetGlobalNS,
10
+    loadScript
11
+} from '../../base/util';
12
+
13
+import { toggleBlurEffect } from '../actions';
14
+
15
+const logger = require('jitsi-meet-logger').getLogger(__filename);
16
+
17
+/**
18
+ * The type of the React {@code Component} props of {@link VideoBlurButton}.
19
+ */
20
+type Props = AbstractButtonProps & {
21
+
22
+    /**
23
+     * True if the video background is blurred or false if it is not.
24
+     */
25
+    _isVideoBlurred: boolean,
26
+
27
+    /**
28
+     * The redux {@code dispatch} function.
29
+     */
30
+    dispatch: Function
31
+
32
+};
33
+
34
+/**
35
+ * An abstract implementation of a button that toggles the video blur effect.
36
+ */
37
+class VideoBlurButton extends AbstractButton<Props, *> {
38
+    accessibilityLabel = 'toolbar.accessibilityLabel.videoblur';
39
+    iconName = 'icon-blur-background';
40
+    label = 'toolbar.startvideoblur';
41
+    tooltip = 'toolbar.startvideoblur';
42
+    toggledLabel = 'toolbar.stopvideoblur';
43
+
44
+    /**
45
+     * Handles clicking / pressing the button, and toggles the blur effect
46
+     * state accordingly.
47
+     *
48
+     * @protected
49
+     * @returns {void}
50
+     */
51
+    _handleClick() {
52
+        const {
53
+            _isVideoBlurred,
54
+            dispatch
55
+        } = this.props;
56
+
57
+        if (!getJitsiMeetGlobalNS().effects
58
+            || !getJitsiMeetGlobalNS().effects.createBlurEffect) {
59
+
60
+            loadScript('libs/video-blur-effect.min.js')
61
+                .then(() => {
62
+                    this._handleClick();
63
+                })
64
+                .catch(error => {
65
+                    logger.error('Failed to load script with error: ', error);
66
+                });
67
+
68
+        } else {
69
+            sendAnalytics(createVideoBlurEvent(_isVideoBlurred ? 'started' : 'stopped'));
70
+
71
+            dispatch(toggleBlurEffect(!_isVideoBlurred));
72
+        }
73
+    }
74
+
75
+    /**
76
+     * Returns {@code boolean} value indicating if the blur effect is
77
+     * enabled or not.
78
+     *
79
+     * @protected
80
+     * @returns {boolean}
81
+     */
82
+    _isToggled() {
83
+        const {
84
+            _isVideoBlurred
85
+        } = this.props;
86
+
87
+        if (!getJitsiMeetGlobalNS().effects
88
+            || !getJitsiMeetGlobalNS().effects.createBlurEffect) {
89
+            return false;
90
+        }
91
+
92
+        return _isVideoBlurred;
93
+    }
94
+}
95
+
96
+/**
97
+ * Maps (parts of) the redux state to the associated props for the
98
+ * {@code VideoBlurButton} component.
99
+ *
100
+ * @param {Object} state - The Redux state.
101
+ * @private
102
+ * @returns {{
103
+ *     _isVideoBlurred: boolean
104
+ * }}
105
+ */
106
+function _mapStateToProps(state): Object {
107
+    return {
108
+        _isVideoBlurred: Boolean(state['features/blur'].blurEnabled)
109
+    };
110
+}
111
+
112
+export default translate(connect(_mapStateToProps)(VideoBlurButton));

+ 1
- 0
react/features/blur/components/index.js ファイルの表示

@@ -0,0 +1 @@
1
+export { default as VideoBlurButton } from './VideoBlurButton';

+ 4
- 0
react/features/blur/index.js ファイルの表示

@@ -0,0 +1,4 @@
1
+export * from './actions';
2
+export * from './components';
3
+
4
+import './reducer';

+ 30
- 0
react/features/blur/reducer.js ファイルの表示

@@ -0,0 +1,30 @@
1
+// @flow
2
+
3
+import { ReducerRegistry } from '../base/redux';
4
+import { PersistenceRegistry } from '../base/storage';
5
+
6
+import { BLUR_ENABLED, BLUR_DISABLED } from './actionTypes';
7
+
8
+PersistenceRegistry.register('features/blur', true, {
9
+    blurEnabled: false
10
+});
11
+
12
+ReducerRegistry.register('features/blur', (state = {}, action) => {
13
+
14
+    switch (action.type) {
15
+    case BLUR_ENABLED: {
16
+        return {
17
+            ...state,
18
+            blurEnabled: true
19
+        };
20
+    }
21
+    case BLUR_DISABLED: {
22
+        return {
23
+            ...state,
24
+            blurEnabled: false
25
+        };
26
+    }
27
+    }
28
+
29
+    return state;
30
+});

+ 237
- 0
react/features/stream-effects/JitsiStreamBlurEffect.js ファイルの表示

@@ -0,0 +1,237 @@
1
+
2
+import { getLogger } from 'jitsi-meet-logger';
3
+import {
4
+    drawBokehEffect,
5
+    load
6
+} from '@tensorflow-models/body-pix';
7
+
8
+import {
9
+    CLEAR_INTERVAL,
10
+    INTERVAL_TIMEOUT,
11
+    SET_INTERVAL,
12
+    timerWorkerScript
13
+} from './TimerWorker';
14
+
15
+const logger = getLogger(__filename);
16
+
17
+/**
18
+ * This promise represents the loading of the BodyPix model that is used
19
+ * to extract person segmentation. A multiplier of 0.25 is used to for
20
+ * improved performance on a larger range of CPUs.
21
+ */
22
+const bpModelPromise = load(0.25);
23
+
24
+/**
25
+ * Represents a modified MediaStream that adds blur to video background.
26
+ * <tt>JitsiStreamBlurEffect</tt> does the processing of the original
27
+ * video stream.
28
+ */
29
+class JitsiStreamBlurEffect {
30
+
31
+    /**
32
+     *
33
+     * Represents a modified video MediaStream track.
34
+     *
35
+     * @class
36
+     * @param {BodyPix} bpModel - BodyPix model
37
+     */
38
+    constructor(bpModel) {
39
+        this._bpModel = bpModel;
40
+
41
+        this._outputCanvasElement = document.createElement('canvas');
42
+        this._maskCanvasElement = document.createElement('canvas');
43
+        this._inputVideoElement = document.createElement('video');
44
+
45
+        this._renderVideo = this._renderVideo.bind(this);
46
+        this._renderMask = this._renderMask.bind(this);
47
+
48
+        this._videoFrameTimerWorker = new Worker(timerWorkerScript);
49
+        this._maskFrameTimerWorker = new Worker(timerWorkerScript);
50
+
51
+        this._onMaskFrameTimer = this._onMaskFrameTimer.bind(this);
52
+        this._onVideoFrameTimer = this._onVideoFrameTimer.bind(this);
53
+        this._videoFrameTimerWorker.onmessage = this._onVideoFrameTimer;
54
+        this._maskFrameTimerWorker.onmessage = this._onMaskFrameTimer;
55
+    }
56
+
57
+    /**
58
+     * EventHandler onmessage for the videoFrameTimerWorker WebWorker
59
+     *
60
+     * @private
61
+     * @param {EventHandler} response - onmessage EventHandler parameter
62
+     * @returns {void}
63
+     */
64
+    _onVideoFrameTimer(response) {
65
+        switch (response.data.id) {
66
+        case INTERVAL_TIMEOUT: {
67
+            this._renderVideo();
68
+            break;
69
+        }
70
+        }
71
+    }
72
+
73
+    /**
74
+     * EventHandler onmessage for the maskFrameTimerWorker WebWorker
75
+     *
76
+     * @private
77
+     * @param {EventHandler} response - onmessage EventHandler parameter
78
+     * @returns {void}
79
+     */
80
+    _onMaskFrameTimer(response) {
81
+        switch (response.data.id) {
82
+        case INTERVAL_TIMEOUT: {
83
+            this._renderMask();
84
+            break;
85
+        }
86
+        }
87
+    }
88
+
89
+    /**
90
+     * Starts loop to capture video frame and render the segmentation mask.
91
+     *
92
+     * @param {MediaStream} stream - Stream to be used for processing
93
+     *
94
+     * @returns {void}
95
+     */
96
+    startEffect(stream) {
97
+        this._stream = stream;
98
+
99
+        const firstVideoTrack = this._stream.getVideoTracks()[0];
100
+        const { height, frameRate, width } = firstVideoTrack.getSettings
101
+            ? firstVideoTrack.getSettings() : firstVideoTrack.getConstraints();
102
+
103
+        if (!firstVideoTrack.getSettings && !firstVideoTrack.getConstraints) {
104
+            throw new Error('JitsiStreamBlurEffect not supported!');
105
+        }
106
+
107
+        this._frameRate = frameRate;
108
+        this._height = height;
109
+        this._width = width;
110
+
111
+        this._outputCanvasElement.width = width;
112
+        this._outputCanvasElement.height = height;
113
+
114
+        this._maskCanvasElement.width = this._width;
115
+        this._maskCanvasElement.height = this._height;
116
+
117
+        this._inputVideoElement.width = width;
118
+        this._inputVideoElement.height = height;
119
+
120
+        this._maskCanvasContext = this._maskCanvasElement.getContext('2d');
121
+
122
+        this._inputVideoElement.autoplay = true;
123
+        this._inputVideoElement.srcObject = this._stream;
124
+
125
+        this._videoFrameTimerWorker.postMessage({
126
+            id: SET_INTERVAL,
127
+            timeMs: 1000 / this._frameRate
128
+        });
129
+
130
+        this._maskFrameTimerWorker.postMessage({
131
+            id: SET_INTERVAL,
132
+            timeMs: 200
133
+        });
134
+    }
135
+
136
+    /**
137
+     * Stops the capture and render loop.
138
+     *
139
+     * @returns {void}
140
+     */
141
+    stopEffect() {
142
+        this._videoFrameTimerWorker.postMessage({
143
+            id: CLEAR_INTERVAL
144
+        });
145
+
146
+        this._maskFrameTimerWorker.postMessage({
147
+            id: CLEAR_INTERVAL
148
+        });
149
+    }
150
+
151
+    /**
152
+     * Get the modified stream.
153
+     *
154
+     * @returns {MediaStream}
155
+     */
156
+    getStreamWithEffect() {
157
+        return this._outputCanvasElement.captureStream(this._frameRate);
158
+    }
159
+
160
+    /**
161
+     * Loop function to render the video frame input and draw blur effect.
162
+     *
163
+     * @private
164
+     * @returns {void}
165
+     */
166
+    _renderVideo() {
167
+        if (this._bpModel) {
168
+            this._maskCanvasContext.drawImage(this._inputVideoElement,
169
+                                                0,
170
+                                                0,
171
+                                                this._width,
172
+                                                this._height);
173
+
174
+            if (this._segmentationData) {
175
+
176
+                drawBokehEffect(this._outputCanvasElement,
177
+                                this._inputVideoElement,
178
+                                this._segmentationData,
179
+                                7, // Constant for background blur, integer values between 0-20
180
+                                7); // Constant for edge blur, integer values between 0-20
181
+            }
182
+        } else {
183
+            this._outputCanvasElement
184
+                .getContext('2d')
185
+                .drawImage(this._inputVideoElement,
186
+                                                0,
187
+                                                0,
188
+                                                this._width,
189
+                                                this._height);
190
+        }
191
+    }
192
+
193
+    /**
194
+     * Loop function to render the background mask.
195
+     *
196
+     * @private
197
+     * @returns {void}
198
+     */
199
+    _renderMask() {
200
+        if (this._bpModel) {
201
+            this._bpModel.estimatePersonSegmentation(this._maskCanvasElement,
202
+                                                    32, // Chose 32 for better performance
203
+                                                    0.75) // Represents probability that a pixel belongs to a person
204
+                .then(value => {
205
+                    this._segmentationData = value;
206
+                });
207
+        }
208
+    }
209
+
210
+    /**
211
+     * Checks if the local track supports this effect.
212
+     *
213
+     * @param {JitsiLocalTrack} jitsiLocalTrack - Track to apply effect
214
+     *
215
+     * @returns {boolean} Returns true if this effect can run on the specified track
216
+     * false otherwise
217
+     */
218
+    isEnabled(jitsiLocalTrack) {
219
+        return jitsiLocalTrack.isVideoTrack();
220
+    }
221
+}
222
+
223
+/**
224
+ * Creates a new instance of JitsiStreamBlurEffect.
225
+ *
226
+ * @returns {Promise<JitsiStreamBlurEffect>}
227
+ */
228
+export function createBlurEffect() {
229
+    return bpModelPromise
230
+        .then(bpmodel =>
231
+            Promise.resolve(new JitsiStreamBlurEffect(bpmodel))
232
+        )
233
+        .catch(error => {
234
+            logger.error('Failed to load BodyPix model. Fallback to original stream!', error);
235
+            throw error;
236
+        });
237
+}

+ 59
- 0
react/features/stream-effects/TimerWorker.js ファイルの表示

@@ -0,0 +1,59 @@
1
+
2
+/**
3
+ * SET_INTERVAL constant is used to set interval and it is set in
4
+ * the id property of the request.data property. timeMs property must
5
+ * also be set. request.data example:
6
+ *
7
+ * {
8
+ *      id: SET_INTERVAL,
9
+ *      timeMs: 33
10
+ * }
11
+ */
12
+export const SET_INTERVAL = 2;
13
+
14
+/**
15
+ * CLEAR_INTERVAL constant is used to clear the interval and it is set in
16
+ * the id property of the request.data property.
17
+ *
18
+ * {
19
+ *      id: CLEAR_INTERVAL
20
+ * }
21
+ */
22
+export const CLEAR_INTERVAL = 3;
23
+
24
+/**
25
+ * INTERVAL_TIMEOUT constant is used as response and it is set in the id property.
26
+ *
27
+ * {
28
+ *      id: INTERVAL_TIMEOUT
29
+ * }
30
+ */
31
+export const INTERVAL_TIMEOUT = 22;
32
+
33
+/**
34
+ * The following code is needed as string to create a URL from a Blob.
35
+ * The URL is then passed to a WebWorker. Reason for this is to enable
36
+ * use of setInterval that is not throttled when tab is inactive.
37
+ */
38
+const code
39
+= `   let timer = null;
40
+
41
+    onmessage = function(request) {
42
+        switch (request.data.id) {
43
+        case ${SET_INTERVAL}: {
44
+            timer = setInterval(() => {
45
+                postMessage({ id: ${INTERVAL_TIMEOUT} });
46
+            }, request.data.timeMs);
47
+            break;
48
+        }
49
+        case ${CLEAR_INTERVAL}: {
50
+            clearInterval(timer);
51
+            break;
52
+        }
53
+        }
54
+    };
55
+`;
56
+
57
+const blob = new Blob([ code ], { type: 'application/javascript' });
58
+
59
+export const timerWorkerScript = URL.createObjectURL(blob);

+ 7
- 1
react/features/toolbox/components/web/Toolbox.js ファイルの表示

@@ -18,6 +18,9 @@ import {
18 18
 import { connect } from '../../../base/redux';
19 19
 import { OverflowMenuItem } from '../../../base/toolbox';
20 20
 import { getLocalVideoTrack, toggleScreensharing } from '../../../base/tracks';
21
+import {
22
+    VideoBlurButton
23
+} from '../../../blur';
21 24
 import { ChatCounter, toggleChat } from '../../../chat';
22 25
 import { toggleDocument } from '../../../etherpad';
23 26
 import { openFeedbackDialog } from '../../../feedback';
@@ -220,7 +223,6 @@ class Toolbox extends Component<Props, State> {
220 223
             = this._onShortcutToggleRaiseHand.bind(this);
221 224
         this._onShortcutToggleScreenshare
222 225
             = this._onShortcutToggleScreenshare.bind(this);
223
-
224 226
         this._onToolbarOpenFeedback
225 227
             = this._onToolbarOpenFeedback.bind(this);
226 228
         this._onToolbarOpenInvite = this._onToolbarOpenInvite.bind(this);
@@ -970,6 +972,10 @@ class Toolbox extends Component<Props, State> {
970 972
                     text = { _editingDocument
971 973
                         ? t('toolbar.documentClose')
972 974
                         : t('toolbar.documentOpen') } />,
975
+            <VideoBlurButton
976
+                key = 'videobackgroundblur'
977
+                showLabel = { true }
978
+                visible = { this._shouldShowButton('videobackgroundblur') } />,
973 979
             <SettingsButton
974 980
                 key = 'settings'
975 981
                 showLabel = { true }

+ 10
- 0
webpack.config.js ファイルの表示

@@ -151,6 +151,16 @@ module.exports = [
151 151
                 './react/features/analytics/handlers/GoogleAnalyticsHandler.js'
152 152
         }
153 153
     }),
154
+    Object.assign({}, config, {
155
+        entry: {
156
+            'video-blur-effect':
157
+                './react/features/stream-effects/JitsiStreamBlurEffect.js'
158
+        },
159
+        output: Object.assign({}, config.output, {
160
+            library: [ 'JitsiMeetJS', 'app', 'effects' ],
161
+            libraryTarget: 'window'
162
+        })
163
+    }),
154 164
 
155 165
     // The Webpack configuration to bundle external_api.js (aka
156 166
     // JitsiMeetExternalAPI).

読み込み中…
キャンセル
保存