Browse Source

Add video background blur

j8
Cristian Florin Ghita 5 years ago
parent
commit
3b750ddd5a

+ 2
- 0
Makefile View File

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

+ 67
- 36
conference.js View File

105
     trackAdded,
105
     trackAdded,
106
     trackRemoved
106
     trackRemoved
107
 } from './react/features/base/tracks';
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
 import { addMessage } from './react/features/chat';
112
 import { addMessage } from './react/features/chat';
110
 import { showDesktopPicker } from './react/features/desktop-picker';
113
 import { showDesktopPicker } from './react/features/desktop-picker';
111
 import { appendSuffix } from './react/features/display-name';
114
 import { appendSuffix } from './react/features/display-name';
559
             // Resolve with no tracks
562
             // Resolve with no tracks
560
             tryCreateLocalTracks = Promise.resolve([]);
563
             tryCreateLocalTracks = Promise.resolve([]);
561
         } else {
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
                         audioOnlyError = err;
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
                         videoOnlyError = err;
628
                         videoOnlyError = err;
578
 
629
 
579
                         return [];
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
         // Hide the permissions prompt/overlay as soon as the tracks are
635
         // Hide the permissions prompt/overlay as soon as the tracks are
649
      */
678
      */
650
     init(options) {
679
     init(options) {
651
         this.roomName = options.roomName;
680
         this.roomName = options.roomName;
681
+        const videoBlurEffectEnabled = APP.store.getState()['features/blur'].blurEnabled;
652
 
682
 
653
         return (
683
         return (
654
 
684
 
662
                     'initial device list initialization failed', error))
692
                     'initial device list initialization failed', error))
663
                 .then(() => this.createInitialLocalTracksAndConnect(
693
                 .then(() => this.createInitialLocalTracksAndConnect(
664
                 options.roomName, {
694
                 options.roomName, {
695
+                    startWithBlurEnabled: videoBlurEffectEnabled,
665
                     startAudioOnly: config.startAudioOnly,
696
                     startAudioOnly: config.startAudioOnly,
666
                     startScreenSharing: config.startScreenSharing,
697
                     startScreenSharing: config.startScreenSharing,
667
                     startWithAudioMuted: config.startWithAudioMuted || config.startSilent,
698
                     startWithAudioMuted: config.startWithAudioMuted || config.startSilent,

+ 4
- 0
css/_font.scss View File

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

BIN
fonts/jitsi.eot View File


+ 1
- 4
fonts/jitsi.svg View File

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" />
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
 <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" />
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
 <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" />
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
 <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" />
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
 <glyph unicode="&#xe904;" glyph-name="kick" d="M512 810l284-426h-568zM214 298h596v-84h-596v84z" />
33
 <glyph unicode="&#xe904;" glyph-name="kick" d="M512 810l284-426h-568zM214 298h596v-84h-596v84z" />
35
 <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" />
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
 <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" />
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
 <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" />
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
 <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" />
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
 <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" />
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
 <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" />
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
 <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" />
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" />

BIN
fonts/jitsi.ttf View File


BIN
fonts/jitsi.woff View File


+ 1
- 1
fonts/selection.json
File diff suppressed because it is too large
View File


+ 1
- 1
interface_config.js View File

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

+ 5
- 2
lang/main.json View File

615
             "speakerStats": "Toggle speaker statistics",
615
             "speakerStats": "Toggle speaker statistics",
616
             "tileView": "Toggle tile view",
616
             "tileView": "Toggle tile view",
617
             "toggleCamera": "Toggle camera",
617
             "toggleCamera": "Toggle camera",
618
-            "videomute": "Toggle mute video"
618
+            "videomute": "Toggle mute video",
619
+            "videoblur": "Toggle video blur"
619
         },
620
         },
620
         "addPeople": "Add people to your call",
621
         "addPeople": "Add people to your call",
621
         "audioonly": "Enable / Disable audio only mode",
622
         "audioonly": "Enable / Disable audio only mode",
668
         "tileViewToggle": "Toggle tile view",
669
         "tileViewToggle": "Toggle tile view",
669
         "toggleCamera": "Toggle camera",
670
         "toggleCamera": "Toggle camera",
670
         "unableToUnmutePopup": "You cannot un-mute while the shared video is on.",
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
     "transcribing": {
676
     "transcribing": {
674
         "ccButtonTooltip": "Start / Stop subtitles",
677
         "ccButtonTooltip": "Start / Stop subtitles",

+ 2
- 0
package.json View File

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

+ 15
- 0
react/features/analytics/AnalyticsEvents.js View File

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
  * Creates an event indicating that an action related to screen sharing
486
  * Creates an event indicating that an action related to screen sharing
472
  * occurred (e.g. It was started or stopped).
487
  * occurred (e.g. It was started or stopped).

+ 1
- 0
react/features/base/tracks/functions.js View File

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

+ 21
- 0
react/features/blur/actionTypes.js View File

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 View File

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 View File

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 View File

1
+export { default as VideoBlurButton } from './VideoBlurButton';

+ 4
- 0
react/features/blur/index.js View File

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

+ 30
- 0
react/features/blur/reducer.js View File

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 View File

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 View File

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 View File

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

+ 10
- 0
webpack.config.js View File

151
                 './react/features/analytics/handlers/GoogleAnalyticsHandler.js'
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
     // The Webpack configuration to bundle external_api.js (aka
165
     // The Webpack configuration to bundle external_api.js (aka
156
     // JitsiMeetExternalAPI).
166
     // JitsiMeetExternalAPI).

Loading…
Cancel
Save