Pārlūkot izejas kodu

feat(face-landmarks): add face landmarks timeline (#12561)

* feat(face-landmarks): add face landmarks timeline

fixes after rebase

* fixes after rebase compiling and linting

* fix: change keyboard shorcut for participants stats

* fix: label for emotions switch

* fix: linting issues

* code review changes

* fix linting issues

* code review changes 2

* fix typo
factor2
Gabriel Borlea 3 gadus atpakaļ
vecāks
revīzija
4b969cf4ab
Revīzijas autora e-pasta adrese nav piesaistīta nevienam kontam
51 mainītis faili ar 1628 papildinājumiem un 800 dzēšanām
  1. 2
    0
      globals.native.d.ts
  2. 4
    4
      lang/main-enGB.json
  3. 4
    4
      lang/main.json
  4. 4
    0
      react/features/base/conference/reducer.ts
  5. 10
    0
      react/features/base/icons/svg/emotions-angry.svg
  6. 10
    0
      react/features/base/icons/svg/emotions-disgusted.svg
  7. 10
    0
      react/features/base/icons/svg/emotions-fearful.svg
  8. 10
    0
      react/features/base/icons/svg/emotions-happy.svg
  9. 10
    0
      react/features/base/icons/svg/emotions-neutral.svg
  10. 10
    0
      react/features/base/icons/svg/emotions-sad.svg
  11. 10
    0
      react/features/base/icons/svg/emotions-surprised.svg
  12. 7
    0
      react/features/base/icons/svg/index.ts
  13. 81
    36
      react/features/face-landmarks/FaceLandmarksDetector.ts
  14. 10
    5
      react/features/face-landmarks/FaceLandmarksHelper.ts
  15. 7
    18
      react/features/face-landmarks/actionTypes.ts
  16. 13
    34
      react/features/face-landmarks/actions.ts
  17. 17
    1
      react/features/face-landmarks/constants.ts
  18. 1
    2
      react/features/face-landmarks/faceLandmarksWorker.ts
  19. 18
    99
      react/features/face-landmarks/functions.ts
  20. 8
    14
      react/features/face-landmarks/middleware.ts
  21. 19
    39
      react/features/face-landmarks/reducer.ts
  22. 17
    1
      react/features/face-landmarks/types.ts
  23. 7
    4
      react/features/rtcstats/middleware.ts
  24. 17
    0
      react/features/speaker-stats/actionTypes.ts
  25. 231
    0
      react/features/speaker-stats/actions.any.ts
  26. 1
    0
      react/features/speaker-stats/actions.native.ts
  27. 0
    94
      react/features/speaker-stats/actions.ts
  28. 1
    0
      react/features/speaker-stats/actions.web.ts
  29. 7
    9
      react/features/speaker-stats/components/AbstractSpeakerStatsButton.tsx
  30. 37
    40
      react/features/speaker-stats/components/AbstractSpeakerStatsList.ts
  31. 1
    0
      react/features/speaker-stats/components/_.native.ts
  32. 0
    0
      react/features/speaker-stats/components/_.web.ts
  33. 1
    0
      react/features/speaker-stats/components/index.ts
  34. 4
    4
      react/features/speaker-stats/components/timeFunctions.ts
  35. 205
    73
      react/features/speaker-stats/components/web/SpeakerStats.tsx
  36. 9
    7
      react/features/speaker-stats/components/web/SpeakerStatsButton.tsx
  37. 0
    136
      react/features/speaker-stats/components/web/SpeakerStatsItem.js
  38. 115
    0
      react/features/speaker-stats/components/web/SpeakerStatsItem.tsx
  39. 10
    33
      react/features/speaker-stats/components/web/SpeakerStatsLabels.tsx
  40. 35
    34
      react/features/speaker-stats/components/web/SpeakerStatsList.tsx
  41. 0
    50
      react/features/speaker-stats/components/web/TimeElapsed.js
  42. 36
    0
      react/features/speaker-stats/components/web/TimeElapsed.tsx
  43. 207
    0
      react/features/speaker-stats/components/web/Timeline.tsx
  44. 187
    0
      react/features/speaker-stats/components/web/TimelineAxis.tsx
  45. 0
    0
      react/features/speaker-stats/components/web/index.ts
  46. 23
    2
      react/features/speaker-stats/constants.ts
  47. 85
    21
      react/features/speaker-stats/functions.ts
  48. 0
    0
      react/features/speaker-stats/index.ts
  49. 38
    7
      react/features/speaker-stats/middleware.ts
  50. 75
    2
      react/features/speaker-stats/reducer.ts
  51. 14
    27
      resources/prosody-plugins/mod_speakerstats_component.lua

+ 2
- 0
globals.native.d.ts Parādīt failu

@@ -23,6 +23,8 @@ interface IWindow {
23 23
     onerror: (event: string, source: any, lineno: any, colno: any, e: Error) => void;
24 24
     onunhandledrejection: (event: any) => void;
25 25
 
26
+    setInterval: typeof setInterval;
27
+    clearInterval: typeof clearInterval;
26 28
     setTimeout: typeof setTimeout;
27 29
     clearTimeout: typeof clearTimeout;
28 30
     setImmediate: typeof setImmediate;

+ 4
- 4
lang/main-enGB.json Parādīt failu

@@ -365,7 +365,7 @@
365 365
         "mute": "Mute or unmute your microphone",
366 366
         "pushToTalk": "Press to transmit",
367 367
         "raiseHand": "Raise or lower your hand",
368
-        "showSpeakerStats": "Show speaker stats",
368
+        "showSpeakerStats": "Show participants stats",
369 369
         "toggleChat": "Open or close the chat",
370 370
         "toggleFilmstrip": "Show or hide video thumbnails",
371 371
         "toggleScreensharing": "Switch between camera and screen sharing",
@@ -579,7 +579,7 @@
579 579
         "minutes": "{{count}}m",
580 580
         "name": "Name",
581 581
         "seconds": "{{count}}s",
582
-        "speakerStats": "Speaker Stats",
582
+        "speakerStats": "Participants Stats",
583 583
         "speakerTime": "Speaker Time"
584 584
     },
585 585
     "startupoverlay": {
@@ -626,7 +626,7 @@
626 626
             "sharedvideo": "Toggle video sharing",
627 627
             "shortcuts": "Toggle shortcuts",
628 628
             "show": "Show on stage",
629
-            "speakerStats": "Toggle speaker statistics",
629
+            "speakerStats": "Toggle participants statistics",
630 630
             "tileView": "Toggle tile view",
631 631
             "toggleCamera": "Toggle camera",
632 632
             "videoblur": "",
@@ -662,7 +662,7 @@
662 662
         "shareRoom": "Invite someone",
663 663
         "sharedvideo": "Share video",
664 664
         "shortcuts": "View shortcuts",
665
-        "speakerStats": "Speaker stats",
665
+        "speakerStats": "Participants stats",
666 666
         "startScreenSharing": "Start screen sharing",
667 667
         "startSubtitles": "Start subtitles",
668 668
         "startvideoblur": "",

+ 4
- 4
lang/main.json Parādīt failu

@@ -511,7 +511,7 @@
511 511
         "mute": "Mute or unmute your microphone",
512 512
         "pushToTalk": "Push to talk",
513 513
         "raiseHand": "Raise or lower your hand",
514
-        "showSpeakerStats": "Show speaker stats",
514
+        "showSpeakerStats": "Show participants stats",
515 515
         "toggleChat": "Open or close the chat",
516 516
         "toggleFilmstrip": "Show or hide video thumbnails",
517 517
         "toggleParticipantsPane": "Show or hide the participants pane",
@@ -1038,7 +1038,7 @@
1038 1038
         "sad": "Sad",
1039 1039
         "search": "Search",
1040 1040
         "seconds": "{{count}}s",
1041
-        "speakerStats": "Speaker Stats",
1041
+        "speakerStats": "Participants Stats",
1042 1042
         "speakerTime": "Speaker Time",
1043 1043
         "surprised": "Surprised"
1044 1044
     },
@@ -1119,7 +1119,7 @@
1119 1119
             "shortcuts": "Toggle shortcuts",
1120 1120
             "show": "Show on stage",
1121 1121
             "silence": "Silence",
1122
-            "speakerStats": "Toggle speaker statistics",
1122
+            "speakerStats": "Toggle participants statistics",
1123 1123
             "surprised": "Surprised",
1124 1124
             "tileView": "Toggle tile view",
1125 1125
             "toggleCamera": "Toggle camera",
@@ -1206,7 +1206,7 @@
1206 1206
         "shortcuts": "View shortcuts",
1207 1207
         "showWhiteboard": "Show whiteboard",
1208 1208
         "silence": "Silence",
1209
-        "speakerStats": "Speaker stats",
1209
+        "speakerStats": "Participants stats",
1210 1210
         "startScreenSharing": "Start screen sharing",
1211 1211
         "startSubtitles": "Subtitles • {{language}}",
1212 1212
         "stopAudioSharing": "Stop audio sharing",

+ 4
- 0
react/features/base/conference/reducer.ts Parādīt failu

@@ -1,4 +1,6 @@
1
+import { FaceLandmarks } from '../../face-landmarks/types';
1 2
 import { LOCKED_LOCALLY, LOCKED_REMOTELY } from '../../room-lock/constants';
3
+import { ISpeakerStats } from '../../speaker-stats/reducer';
2 4
 import { CONNECTION_WILL_CONNECT, SET_LOCATION_URL } from '../connection/actionTypes';
3 5
 import { JitsiConferenceErrors } from '../lib-jitsi-meet';
4 6
 import ReducerRegistry from '../redux/ReducerRegistry';
@@ -53,6 +55,7 @@ export interface IJitsiConference {
53 55
     getMeetingUniqueId: Function;
54 56
     getParticipantById: Function;
55 57
     getParticipants: Function;
58
+    getSpeakerStats: () => ISpeakerStats;
56 59
     grantOwner: Function;
57 60
     isAVModerationSupported: Function;
58 61
     isCallstatsEnabled: Function;
@@ -74,6 +77,7 @@ export interface IJitsiConference {
74 77
     sendCommand: Function;
75 78
     sendCommandOnce: Function;
76 79
     sendEndpointMessage: Function;
80
+    sendFaceLandmarks: (faceLandmarks: FaceLandmarks) => void;
77 81
     sendFeedback: Function;
78 82
     sendLobbyMessage: Function;
79 83
     sessionId: string;

+ 10
- 0
react/features/base/icons/svg/emotions-angry.svg Parādīt failu

@@ -0,0 +1,10 @@
1
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+<circle cx="10" cy="10" r="10" fill="url(#paint0_radial_72_1897)"/>
3
+<path opacity="0.8" fill-rule="evenodd" clip-rule="evenodd" d="M5.46845 5.14411C5.08781 4.88541 4.56951 4.98428 4.31081 5.36492C4.05212 5.74557 4.15098 6.26387 4.53163 6.52257L5.47766 7.16551C5.18224 7.46624 5.00002 7.87855 5.00002 8.33341C5.00002 9.25388 5.74622 10.0001 6.66669 10.0001C7.58716 10.0001 8.33336 9.25388 8.33336 8.33341C8.33336 8.23415 8.32468 8.13691 8.30804 8.04242C8.54426 7.66462 8.44124 7.16449 8.06956 6.91188L5.46845 5.14411ZM6.66305 14.7842C6.30373 14.5781 6.1795 14.1198 6.38556 13.7605C6.75032 13.1244 7.27645 12.5959 7.91081 12.2283C8.54518 11.8607 9.26532 11.6669 9.99852 11.6667C10.7317 11.6664 11.452 11.8596 12.0866 12.2268C12.7213 12.5939 13.2478 13.1221 13.613 13.7578C13.8193 14.117 13.6954 14.5754 13.3362 14.7818C12.9771 14.9881 12.5186 14.8642 12.3123 14.505C12.0786 14.0981 11.7416 13.7601 11.3354 13.5251C10.9293 13.2901 10.4683 13.1665 9.99906 13.1667C9.52981 13.1668 9.06892 13.2908 8.66293 13.5261C8.25693 13.7614 7.92021 14.0996 7.68677 14.5067C7.4807 14.866 7.02237 14.9902 6.66305 14.7842ZM15.7903 5.36492C15.5316 4.98428 15.0134 4.88541 14.6327 5.14411L12.0316 6.91188C11.7043 7.13434 11.5853 7.54876 11.7229 7.90254C11.6862 8.03998 11.6667 8.18441 11.6667 8.33341C11.6667 9.25388 12.4129 10.0001 13.3334 10.0001C14.2538 10.0001 15 9.25388 15 8.33341C15 7.89926 14.834 7.50388 14.562 7.20728L15.5695 6.52257C15.9502 6.26387 16.049 5.74557 15.7903 5.36492Z" fill="black"/>
4
+<defs>
5
+<radialGradient id="paint0_radial_72_1897" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(10 4.58333) rotate(90) scale(15.4167)">
6
+<stop offset="0.359375" stop-color="#F26325"/>
7
+<stop offset="1" stop-color="#F24A25"/>
8
+</radialGradient>
9
+</defs>
10
+</svg>

+ 10
- 0
react/features/base/icons/svg/emotions-disgusted.svg Parādīt failu

@@ -0,0 +1,10 @@
1
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+<circle cx="10" cy="10" r="10" fill="url(#paint0_radial_351_6183)"/>
3
+<path opacity="0.8" fill-rule="evenodd" clip-rule="evenodd" d="M4.16669 7.50001C4.16669 7.03977 4.53978 6.66667 5.00002 6.66667H7.50002C7.96026 6.66667 8.33335 7.03977 8.33335 7.50001C8.33335 7.96024 7.96026 8.33334 7.50002 8.33334H5.00002C4.53978 8.33334 4.16669 7.96024 4.16669 7.50001ZM6.66669 15C6.66669 13.1591 8.15907 11.6667 10 11.6667C11.841 11.6667 13.3334 13.1591 13.3334 15H6.66669ZM12.5 6.66667C12.0398 6.66667 11.6667 7.03977 11.6667 7.50001C11.6667 7.96024 12.0398 8.33334 12.5 8.33334H15C15.4603 8.33334 15.8334 7.96024 15.8334 7.50001C15.8334 7.03977 15.4603 6.66667 15 6.66667H12.5Z" fill="black"/>
4
+<defs>
5
+<radialGradient id="paint0_radial_351_6183" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(10 4.58333) rotate(90) scale(15.4167)">
6
+<stop offset="0.359375" stop-color="#98E791"/>
7
+<stop offset="1" stop-color="#3C9845"/>
8
+</radialGradient>
9
+</defs>
10
+</svg>

+ 10
- 0
react/features/base/icons/svg/emotions-fearful.svg Parādīt failu

@@ -0,0 +1,10 @@
1
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+<circle cx="10" cy="10" r="10" fill="url(#paint0_radial_72_1884)"/>
3
+<path opacity="0.8" fill-rule="evenodd" clip-rule="evenodd" d="M8.33333 7.49999C8.33333 8.42047 7.58714 9.16666 6.66667 9.16666C5.74619 9.16666 5 8.42047 5 7.49999C5 6.57952 5.74619 5.83333 6.66667 5.83333C7.58714 5.83333 8.33333 6.57952 8.33333 7.49999ZM15 7.49999C15 8.42047 14.2538 9.16666 13.3333 9.16666C12.4129 9.16666 11.6667 8.42047 11.6667 7.49999C11.6667 6.57952 12.4129 5.83333 13.3333 5.83333C14.2538 5.83333 15 6.57952 15 7.49999ZM10 11.6667C8.15905 11.6667 6.66667 13.159 6.66667 15H13.3333C13.3333 13.159 11.8409 11.6667 10 11.6667Z" fill="black"/>
4
+<defs>
5
+<radialGradient id="paint0_radial_72_1884" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(10 4.58333) rotate(90) scale(15.4167)">
6
+<stop offset="0.359375" stop-color="#6BEBD4"/>
7
+<stop offset="1" stop-color="#077EA4"/>
8
+</radialGradient>
9
+</defs>
10
+</svg>

+ 10
- 0
react/features/base/icons/svg/emotions-happy.svg Parādīt failu

@@ -0,0 +1,10 @@
1
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+<circle cx="10" cy="10" r="10" fill="url(#paint0_radial_72_1844)"/>
3
+<path opacity="0.8" fill-rule="evenodd" clip-rule="evenodd" d="M8.33333 7.49999C8.33333 8.42047 7.58714 9.16666 6.66667 9.16666C5.74619 9.16666 5 8.42047 5 7.49999C5 6.57952 5.74619 5.83333 6.66667 5.83333C7.58714 5.83333 8.33333 6.57952 8.33333 7.49999ZM15 7.49999C15 8.42047 14.2538 9.16666 13.3333 9.16666C12.4129 9.16666 11.6667 8.42047 11.6667 7.49999C11.6667 6.57952 12.4129 5.83333 13.3333 5.83333C14.2538 5.83333 15 6.57952 15 7.49999ZM7.53238 12.6776C7.37535 12.2943 6.93734 12.1109 6.55404 12.2679C6.17075 12.4249 5.98732 12.8629 6.14435 13.2462C6.45676 14.0088 6.98828 14.6616 7.6717 15.1221C8.35513 15.5826 9.15976 15.8301 9.98384 15.8333C10.8079 15.8365 11.6144 15.5953 12.3014 15.1401C12.9884 14.6849 13.525 14.0362 13.8433 13.2761C14.0033 12.894 13.8233 12.4546 13.4412 12.2946C13.0591 12.1346 12.6197 12.3146 12.4597 12.6967C12.256 13.1832 11.9126 13.5983 11.4729 13.8896C11.0332 14.181 10.5171 14.3354 9.98966 14.3333C9.46224 14.3313 8.94728 14.1729 8.50989 13.8782C8.0725 13.5834 7.73232 13.1656 7.53238 12.6776Z" fill="black"/>
4
+<defs>
5
+<radialGradient id="paint0_radial_72_1844" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(10 4.58333) rotate(90) scale(15.4167)">
6
+<stop offset="0.359375" stop-color="#F2AD25"/>
7
+<stop offset="1" stop-color="#F27B25"/>
8
+</radialGradient>
9
+</defs>
10
+</svg>

+ 10
- 0
react/features/base/icons/svg/emotions-neutral.svg Parādīt failu

@@ -0,0 +1,10 @@
1
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+<circle cx="10" cy="10" r="10" fill="url(#paint0_radial_72_1850)"/>
3
+<path opacity="0.8" fill-rule="evenodd" clip-rule="evenodd" d="M8.33333 7.49999C8.33333 8.42047 7.58714 9.16666 6.66667 9.16666C5.74619 9.16666 5 8.42047 5 7.49999C5 6.57952 5.74619 5.83333 6.66667 5.83333C7.58714 5.83333 8.33333 6.57952 8.33333 7.49999ZM15 7.49999C15 8.42047 14.2538 9.16666 13.3333 9.16666C12.4129 9.16666 11.6667 8.42047 11.6667 7.49999C11.6667 6.57952 12.4129 5.83333 13.3333 5.83333C14.2538 5.83333 15 6.57952 15 7.49999ZM7.5 13.3333C7.03976 13.3333 6.66667 13.7064 6.66667 14.1667C6.66667 14.6269 7.03976 15 7.5 15H12.5C12.9602 15 13.3333 14.6269 13.3333 14.1667C13.3333 13.7064 12.9602 13.3333 12.5 13.3333H7.5Z" fill="black"/>
4
+<defs>
5
+<radialGradient id="paint0_radial_72_1850" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(10 4.58333) rotate(90) scale(15.4167)">
6
+<stop offset="0.359375" stop-color="#AAAAAA"/>
7
+<stop offset="1" stop-color="#5E5E5E"/>
8
+</radialGradient>
9
+</defs>
10
+</svg>

+ 10
- 0
react/features/base/icons/svg/emotions-sad.svg Parādīt failu

@@ -0,0 +1,10 @@
1
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+<circle cx="10" cy="10" r="10" fill="url(#paint0_radial_72_1862)"/>
3
+<path opacity="0.8" fill-rule="evenodd" clip-rule="evenodd" d="M8.33333 7.49999C8.33333 8.42047 7.58714 9.16666 6.66667 9.16666C5.74619 9.16666 5 8.42047 5 7.49999C5 6.57952 5.74619 5.83333 6.66667 5.83333C7.58714 5.83333 8.33333 6.57952 8.33333 7.49999ZM15 7.49999C15 8.42047 14.2538 9.16666 13.3333 9.16666C12.4129 9.16666 11.6667 8.42047 11.6667 7.49999C11.6667 6.57952 12.4129 5.83333 13.3333 5.83333C14.2538 5.83333 15 6.57952 15 7.49999ZM6.38554 13.7605C6.17948 14.1198 6.30371 14.5781 6.66303 14.7842C7.02235 14.9902 7.48068 14.866 7.68675 14.5067C7.92019 14.0996 8.25691 13.7614 8.66291 13.5261C9.0689 13.2908 9.52979 13.1668 9.99904 13.1667C10.4683 13.1665 10.9293 13.2901 11.3354 13.5251C11.7416 13.7601 12.0786 14.0981 12.3123 14.505C12.5186 14.8642 12.977 14.9881 13.3362 14.7818C13.6954 14.5754 13.8193 14.117 13.613 13.7578C13.2477 13.1221 12.7212 12.5939 12.0866 12.2268C11.452 11.8596 10.7317 11.6664 9.9985 11.6667C9.2653 11.6669 8.54516 11.8607 7.91079 12.2283C7.27643 12.5959 6.7503 13.1244 6.38554 13.7605Z" fill="black"/>
4
+<defs>
5
+<radialGradient id="paint0_radial_72_1862" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(10 4.58333) rotate(90) scale(15.4167)">
6
+<stop offset="0.359375" stop-color="#65B3FB"/>
7
+<stop offset="1" stop-color="#256BF2"/>
8
+</radialGradient>
9
+</defs>
10
+</svg>

+ 10
- 0
react/features/base/icons/svg/emotions-surprised.svg Parādīt failu

@@ -0,0 +1,10 @@
1
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+<circle cx="10" cy="10" r="10" fill="url(#paint0_radial_72_1873)"/>
3
+<path fill-rule="evenodd" clip-rule="evenodd" d="M8.33333 7.49999C8.33333 8.42047 7.58714 9.16666 6.66667 9.16666C5.74619 9.16666 5 8.42047 5 7.49999C5 6.57952 5.74619 5.83333 6.66667 5.83333C7.58714 5.83333 8.33333 6.57952 8.33333 7.49999ZM15 7.49999C15 8.42047 14.2538 9.16666 13.3333 9.16666C12.4129 9.16666 11.6667 8.42047 11.6667 7.49999C11.6667 6.57952 12.4129 5.83333 13.3333 5.83333C14.2538 5.83333 15 6.57952 15 7.49999ZM10 15C11.3807 15 12.5 14.403 12.5 13.6667C12.5 12.9303 11.3807 11.6667 10 11.6667C8.61929 11.6667 7.5 12.9303 7.5 13.6667C7.5 14.403 8.61929 15 10 15Z" fill="black"/>
4
+<defs>
5
+<radialGradient id="paint0_radial_72_1873" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(10 4.58333) rotate(90) scale(15.4167)">
6
+<stop offset="0.359375" stop-color="#CC86E4"/>
7
+<stop offset="1" stop-color="#933CD8"/>
8
+</radialGradient>
9
+</defs>
10
+</svg>

+ 7
- 0
react/features/base/icons/svg/index.ts Parādīt failu

@@ -29,6 +29,13 @@ export { default as IconE2EE } from './e2ee.svg';
29 29
 export { default as IconEnlarge } from './enlarge.svg';
30 30
 export { default as IconEnterFullscreen } from './enter-fullscreen.svg';
31 31
 export { default as IconEnvelope } from './envelope.svg';
32
+export { default as IconEmotionsAngry } from './emotions-angry.svg';
33
+export { default as IconEmotionsDisgusted } from './emotions-disgusted.svg';
34
+export { default as IconEmotionsFearful } from './emotions-fearful.svg';
35
+export { default as IconEmotionsHappy } from './emotions-happy.svg';
36
+export { default as IconEmotionsNeutral } from './emotions-neutral.svg';
37
+export { default as IconEmotionsSad } from './emotions-sad.svg';
38
+export { default as IconEmotionsSurprised } from './emotions-surprised.svg';
32 39
 export { default as IconExclamationSolid } from './exclamation-solid.svg';
33 40
 export { default as IconExclamationTriangle } from './exclamation-triangle.svg';
34 41
 export { default as IconExitFullscreen } from './exit-fullscreen.svg';

+ 81
- 36
react/features/face-landmarks/FaceLandmarksDetector.ts Parādīt failu

@@ -5,20 +5,21 @@ import { getLocalVideoTrack } from '../base/tracks/functions';
5 5
 import { getBaseUrl } from '../base/util/helpers';
6 6
 
7 7
 import {
8
-    addFaceExpression,
8
+    addFaceLandmarks,
9 9
     clearFaceExpressionBuffer,
10 10
     newFaceBox
11 11
 } from './actions';
12 12
 import {
13 13
     DETECTION_TYPES,
14 14
     DETECT_FACE,
15
-    FACE_LANDMARK_DETECTION_ERROR_THRESHOLD,
15
+    FACE_LANDMARKS_DETECTION_ERROR_THRESHOLD,
16 16
     INIT_WORKER,
17
+    NO_DETECTION,
18
+    NO_FACE_DETECTION_THRESHOLD,
17 19
     WEBHOOK_SEND_TIME_INTERVAL
18 20
 } from './constants';
19 21
 import {
20 22
     getDetectionInterval,
21
-    getFaceExpressionDuration,
22 23
     sendFaceExpressionsWebhook
23 24
 } from './functions';
24 25
 import logger from './logger';
@@ -33,13 +34,14 @@ class FaceLandmarksDetector {
33 34
     private worker: Worker | null = null;
34 35
     private lastFaceExpression: string | null = null;
35 36
     private lastFaceExpressionTimestamp: number | null = null;
36
-    private duplicateConsecutiveExpressions = 0;
37 37
     private webhookSendInterval: number | null = null;
38 38
     private detectionInterval: number | null = null;
39 39
     private recognitionActive = false;
40 40
     private canvas?: HTMLCanvasElement;
41 41
     private context?: CanvasRenderingContext2D | null;
42 42
     private errorCount = 0;
43
+    private noDetectionCount = 0;
44
+    private noDetectionStartTimestamp: number | null = null;
43 45
 
44 46
     /**
45 47
      * Constructor for class, checks if the environment supports OffscreenCanvas.
@@ -97,27 +99,48 @@ class FaceLandmarksDetector {
97 99
 
98 100
         // @ts-ignore
99 101
         const workerBlob = new Blob([ `importScripts("${workerUrl}");` ], { type: 'application/javascript' });
102
+        const state = getState();
103
+        const addToBuffer = Boolean(state['features/base/config'].webhookProxyUrl);
100 104
 
101 105
         // @ts-ignore
102 106
         workerUrl = window.URL.createObjectURL(workerBlob);
103
-        this.worker = new Worker(workerUrl, { name: 'Face Recognition Worker' });
107
+        this.worker = new Worker(workerUrl, { name: 'Face Landmarks Worker' });
104 108
         this.worker.onmessage = ({ data }: MessageEvent<any>) => {
105
-            const { faceExpression, faceBox } = data;
106
-
107
-            if (faceExpression) {
108
-                if (faceExpression === this.lastFaceExpression) {
109
-                    this.duplicateConsecutiveExpressions++;
110
-                } else {
111
-                    if (this.lastFaceExpression && this.lastFaceExpressionTimestamp) {
112
-                        dispatch(addFaceExpression(
113
-                            this.lastFaceExpression,
114
-                            getFaceExpressionDuration(getState(), this.duplicateConsecutiveExpressions + 1),
115
-                            this.lastFaceExpressionTimestamp
116
-                        ));
117
-                    }
118
-                    this.lastFaceExpression = faceExpression;
119
-                    this.lastFaceExpressionTimestamp = Date.now();
120
-                    this.duplicateConsecutiveExpressions = 0;
109
+            const { faceExpression, faceBox, faceCount } = data;
110
+            const messageTimestamp = Date.now();
111
+
112
+            // if the number of faces detected is different from 1 we do not take into consideration that detection
113
+            if (faceCount !== 1) {
114
+                if (this.noDetectionCount === 0) {
115
+                    this.noDetectionStartTimestamp = messageTimestamp;
116
+                }
117
+                this.noDetectionCount++;
118
+
119
+                if (this.noDetectionCount === NO_FACE_DETECTION_THRESHOLD && this.noDetectionStartTimestamp) {
120
+                    this.addFaceLandmarks(
121
+                            dispatch,
122
+                            this.noDetectionStartTimestamp,
123
+                            NO_DETECTION,
124
+                            addToBuffer
125
+                    );
126
+                }
127
+
128
+                return;
129
+            } else if (this.noDetectionCount > 0) {
130
+                this.noDetectionCount = 0;
131
+                this.noDetectionStartTimestamp = null;
132
+            }
133
+
134
+            if (faceExpression?.expression) {
135
+                const { expression } = faceExpression;
136
+
137
+                if (expression !== this.lastFaceExpression) {
138
+                    this.addFaceLandmarks(
139
+                        dispatch,
140
+                        messageTimestamp,
141
+                        expression,
142
+                        addToBuffer
143
+                    );
121 144
                 }
122 145
             }
123 146
 
@@ -128,7 +151,7 @@ class FaceLandmarksDetector {
128 151
             APP.API.notifyFaceLandmarkDetected(faceBox, faceExpression);
129 152
         };
130 153
 
131
-        const { faceLandmarks } = getState()['features/base/config'];
154
+        const { faceLandmarks } = state['features/base/config'];
132 155
         const detectionTypes = [
133 156
             faceLandmarks?.enableFaceCentering && DETECTION_TYPES.FACE_BOX,
134 157
             faceLandmarks?.enableFaceExpressionsDetection && DETECTION_TYPES.FACE_EXPRESSIONS
@@ -162,7 +185,7 @@ class FaceLandmarksDetector {
162 185
         }
163 186
 
164 187
         if (this.recognitionActive) {
165
-            logger.log('Face detection already active.');
188
+            logger.log('Face landmarks detection already active.');
166 189
 
167 190
             return;
168 191
         }
@@ -179,7 +202,7 @@ class FaceLandmarksDetector {
179 202
 
180 203
         this.imageCapture = new ImageCapture(firstVideoTrack);
181 204
         this.recognitionActive = true;
182
-        logger.log('Start face detection');
205
+        logger.log('Start face landmarks detection');
183 206
 
184 207
         const { faceLandmarks } = state['features/base/config'];
185 208
 
@@ -191,7 +214,7 @@ class FaceLandmarksDetector {
191 214
                 ).then(status => {
192 215
                     if (status) {
193 216
                         this.errorCount = 0;
194
-                    } else if (++this.errorCount > FACE_LANDMARK_DETECTION_ERROR_THRESHOLD) {
217
+                    } else if (++this.errorCount > FACE_LANDMARKS_DETECTION_ERROR_THRESHOLD) {
195 218
                         /* this prevents the detection from stopping immediately after occurring an error
196 219
                          * sometimes due to the small detection interval when starting the detection some errors
197 220
                          * might occur due to the track not being ready
@@ -228,18 +251,11 @@ class FaceLandmarksDetector {
228 251
         if (!this.recognitionActive || !this.isInitialized()) {
229 252
             return;
230 253
         }
254
+        const stopTimestamp = Date.now();
255
+        const addToBuffer = Boolean(getState()['features/base/config'].webhookProxyUrl);
231 256
 
232 257
         if (this.lastFaceExpression && this.lastFaceExpressionTimestamp) {
233
-            dispatch(
234
-                addFaceExpression(
235
-                    this.lastFaceExpression,
236
-                    getFaceExpressionDuration(getState(), this.duplicateConsecutiveExpressions + 1),
237
-                    this.lastFaceExpressionTimestamp
238
-                )
239
-            );
240
-            this.duplicateConsecutiveExpressions = 0;
241
-            this.lastFaceExpression = null;
242
-            this.lastFaceExpressionTimestamp = null;
258
+            this.addFaceLandmarks(dispatch, stopTimestamp, null, addToBuffer);
243 259
         }
244 260
 
245 261
         this.webhookSendInterval && window.clearInterval(this.webhookSendInterval);
@@ -248,7 +264,36 @@ class FaceLandmarksDetector {
248 264
         this.detectionInterval = null;
249 265
         this.imageCapture = null;
250 266
         this.recognitionActive = false;
251
-        logger.log('Stop face detection');
267
+        logger.log('Stop face landmarks detection');
268
+    }
269
+
270
+    /**
271
+     * Dispatches the action for adding new face landmarks and changes the state of the class.
272
+     *
273
+     * @param {IStore.dispatch} dispatch - The redux dispatch function.
274
+     * @param {number} endTimestamp - The timestamp when the face landmarks ended.
275
+     * @param {string} newFaceExpression - The new face expression.
276
+     * @param {boolean} addToBuffer - Flag for adding the face landmarks to the buffer.
277
+     * @returns {void}
278
+     */
279
+    private addFaceLandmarks(
280
+            dispatch: IStore['dispatch'],
281
+            endTimestamp: number,
282
+            newFaceExpression: string | null,
283
+            addToBuffer = false) {
284
+        if (this.lastFaceExpression && this.lastFaceExpressionTimestamp) {
285
+            dispatch(addFaceLandmarks(
286
+                {
287
+                    duration: endTimestamp - this.lastFaceExpressionTimestamp,
288
+                    faceExpression: this.lastFaceExpression,
289
+                    timestamp: this.lastFaceExpressionTimestamp
290
+                },
291
+                addToBuffer
292
+            ));
293
+        }
294
+
295
+        this.lastFaceExpression = newFaceExpression;
296
+        this.lastFaceExpressionTimestamp = endTimestamp;
252 297
     }
253 298
 
254 299
     /**

+ 10
- 5
react/features/face-landmarks/FaceLandmarksHelper.ts Parādīt failu

@@ -2,7 +2,7 @@ import { setWasmPaths } from '@tensorflow/tfjs-backend-wasm';
2 2
 import { Config, FaceResult, Human } from '@vladmandic/human';
3 3
 
4 4
 import { DETECTION_TYPES, FACE_DETECTION_SCORE_THRESHOLD, FACE_EXPRESSIONS_NAMING_MAPPING } from './constants';
5
-import { DetectInput, DetectOutput, FaceBox, InitInput } from './types';
5
+import { DetectInput, DetectOutput, FaceBox, FaceExpression, InitInput } from './types';
6 6
 
7 7
 export interface IFaceLandmarksHelper {
8 8
     detect: ({ image, threshold }: DetectInput) => Promise<DetectOutput>;
@@ -10,7 +10,7 @@ export interface IFaceLandmarksHelper {
10 10
     getDetections: (image: ImageBitmap | ImageData) => Promise<Array<FaceResult>>;
11 11
     getFaceBox: (detections: Array<FaceResult>, threshold: number) => FaceBox | undefined;
12 12
     getFaceCount: (detections: Array<FaceResult>) => number;
13
-    getFaceExpression: (detections: Array<FaceResult>) => string | undefined;
13
+    getFaceExpression: (detections: Array<FaceResult>) => FaceExpression | undefined;
14 14
     init: () => Promise<void>;
15 15
 }
16 16
 
@@ -144,13 +144,18 @@ export class HumanHelper implements IFaceLandmarksHelper {
144 144
      * @param {Array<FaceResult>} detections - The array with the detections.
145 145
      * @returns {string | undefined}
146 146
      */
147
-    getFaceExpression(detections: Array<FaceResult>): string | undefined {
147
+    getFaceExpression(detections: Array<FaceResult>): FaceExpression | undefined {
148 148
         if (this.getFaceCount(detections) !== 1) {
149 149
             return;
150 150
         }
151 151
 
152
-        if (detections[0].emotion) {
153
-            return FACE_EXPRESSIONS_NAMING_MAPPING[detections[0].emotion[0].emotion];
152
+        const detection = detections[0];
153
+
154
+        if (detection.emotion) {
155
+            return {
156
+                expression: FACE_EXPRESSIONS_NAMING_MAPPING[detection.emotion[0].emotion],
157
+                score: detection.emotion[0].score
158
+            };
154 159
         }
155 160
     }
156 161
 

+ 7
- 18
react/features/face-landmarks/actionTypes.ts Parādīt failu

@@ -1,32 +1,21 @@
1 1
 /**
2
- * Redux action type dispatched in order to add a face expression.
2
+ * Redux action type dispatched in order to add real-time faceLandmarks to timeline.
3 3
  *
4 4
  * {
5
- *      type: ADD_FACE_EXPRESSION,
6
- *      faceExpression: string,
7
- *      duration: number
5
+ *      type: ADD_FACE_LANDMARKS,
6
+ *      faceLandmarks: FaceLandmarks
8 7
  * }
9 8
  */
10
-export const ADD_FACE_EXPRESSION = 'ADD_FACE_EXPRESSION';
9
+export const ADD_FACE_LANDMARKS = 'ADD_FACE_LANDMARKS';
11 10
 
12 11
 /**
13
- * Redux action type dispatched in order to add a expression to the face expressions buffer.
12
+ * Redux action type dispatched in order to clear the faceLandmarks buffer for webhook in the state.
14 13
  *
15 14
  * {
16
- *      type: ADD_TO_FACE_EXPRESSIONS_BUFFER,
17
- *      faceExpression: string
15
+ *      type: CLEAR_FACE_LANDMARKS_BUFFER
18 16
  * }
19 17
 */
20
-export const ADD_TO_FACE_EXPRESSIONS_BUFFER = 'ADD_TO_FACE_EXPRESSIONS_BUFFER';
21
-
22
-/**
23
- * Redux action type dispatched in order to clear the face expressions buffer in the state.
24
- *
25
- * {
26
- *      type: CLEAR_FACE_EXPRESSIONS_BUFFER
27
- * }
28
-*/
29
-export const CLEAR_FACE_EXPRESSIONS_BUFFER = 'CLEAR_FACE_EXPRESSIONS_BUFFER';
18
+export const CLEAR_FACE_LANDMARKS_BUFFER = 'CLEAR_FACE_LANDMARKS_BUFFER';
30 19
 
31 20
 /**
32 21
  * Redux action type dispatched in order to update coordinates of a detected face.

+ 13
- 34
react/features/face-landmarks/actions.ts Parādīt failu

@@ -3,56 +3,35 @@ import './createImageBitmap';
3 3
 import { AnyAction } from 'redux';
4 4
 
5 5
 import {
6
-    ADD_FACE_EXPRESSION,
7
-    ADD_TO_FACE_EXPRESSIONS_BUFFER,
8
-    CLEAR_FACE_EXPRESSIONS_BUFFER,
6
+    ADD_FACE_LANDMARKS,
7
+    CLEAR_FACE_LANDMARKS_BUFFER,
9 8
     NEW_FACE_COORDINATES
10 9
 } from './actionTypes';
11
-import { FaceBox } from './types';
10
+import { FaceBox, FaceLandmarks } from './types';
12 11
 
13 12
 /**
14
- * Adds a new face expression and its duration.
13
+ * Adds new face landmarks to the timeline.
15 14
  *
16
- * @param  {string} faceExpression - Face expression to be added.
17
- * @param  {number} duration - Duration in seconds of the face expression.
18
- * @param  {number} timestamp - Duration in seconds of the face expression.
15
+ * @param {FaceLandmarks} faceLandmarks - The new face landmarks to timeline.
16
+ * @param {boolean} addToBuffer - If true adds the face landmarks to a buffer in the reducer for webhook.
19 17
  * @returns {AnyAction}
20 18
  */
21
-export function addFaceExpression(faceExpression: string, duration: number, timestamp: number): AnyAction {
19
+export function addFaceLandmarks(faceLandmarks: FaceLandmarks, addToBuffer: boolean): AnyAction {
22 20
     return {
23
-        type: ADD_FACE_EXPRESSION,
24
-        faceExpression,
25
-        duration,
26
-        timestamp
21
+        type: ADD_FACE_LANDMARKS,
22
+        faceLandmarks,
23
+        addToBuffer
27 24
     };
28 25
 }
29 26
 
30 27
 /**
31
- * Adds a face expression with its timestamp to the face expression buffer.
28
+ * Clears the face landmarks array in the state.
32 29
  *
33
- * @param {Object} faceExpression - Object containing face expression string and its timestamp.
34 30
  * @returns {AnyAction}
35 31
  */
36
-export function addToFaceExpressionsBuffer(
37
-        faceExpression: {
38
-            emotion: string;
39
-            timestamp: number;
40
-        }
41
-): AnyAction {
32
+export function clearFaceExpressionBuffer(): AnyAction {
42 33
     return {
43
-        type: ADD_TO_FACE_EXPRESSIONS_BUFFER,
44
-        faceExpression
45
-    };
46
-}
47
-
48
-/**
49
- * Clears the face expressions array in the state.
50
- *
51
- * @returns {Object}
52
- */
53
-export function clearFaceExpressionBuffer() {
54
-    return {
55
-        type: CLEAR_FACE_EXPRESSIONS_BUFFER
34
+        type: CLEAR_FACE_LANDMARKS_BUFFER
56 35
     };
57 36
 }
58 37
 

+ 17
- 1
react/features/face-landmarks/constants.ts Parādīt failu

@@ -37,6 +37,11 @@ export const INIT_WORKER = 'INIT_WORKER';
37 37
  */
38 38
 export const FACE_BOX_EVENT_TYPE = 'face-box';
39 39
 
40
+/**
41
+ * Type of event sent on the data channel.
42
+ */
43
+export const FACE_LANDMARKS_EVENT_TYPE = 'face-landmarks';
44
+
40 45
 /**
41 46
  * Milliseconds interval value for sending new image data to the worker.
42 47
  */
@@ -64,4 +69,15 @@ export const FACE_DETECTION_SCORE_THRESHOLD = 0.75;
64 69
 /**
65 70
  * Threshold for stopping detection after a certain number of consecutive errors have occurred.
66 71
  */
67
-export const FACE_LANDMARK_DETECTION_ERROR_THRESHOLD = 4;
72
+export const FACE_LANDMARKS_DETECTION_ERROR_THRESHOLD = 4;
73
+
74
+/**
75
+ * Threshold for number of consecutive detections with no face,
76
+ * so that when achieved there will be dispatched an action.
77
+ */
78
+export const NO_FACE_DETECTION_THRESHOLD = 5;
79
+
80
+/**
81
+ * Constant type used for signaling that no valid face detection is found.
82
+ */
83
+export const NO_DETECTION = 'no-detection';

+ 1
- 2
react/features/face-landmarks/faceLandmarksWorker.ts Parādīt failu

@@ -12,10 +12,9 @@ onmessage = async function({ data }: MessageEvent<any>) {
12 12
 
13 13
         const detections = await helper.detect(data);
14 14
 
15
-        if (detections && (detections.faceBox || detections.faceExpression || detections.faceCount)) {
15
+        if (detections) {
16 16
             self.postMessage(detections);
17 17
         }
18
-
19 18
         break;
20 19
     }
21 20
 

+ 18
- 99
react/features/face-landmarks/functions.ts Parādīt failu

@@ -1,40 +1,27 @@
1 1
 import { IReduxState } from '../app/types';
2
+import { IJitsiConference } from '../base/conference/reducer';
2 3
 import { getLocalParticipant } from '../base/participants/functions';
3 4
 import { extractFqnFromPath } from '../dynamic-branding/functions.any';
4 5
 
5
-import { DETECT_FACE, FACE_BOX_EVENT_TYPE, SEND_IMAGE_INTERVAL_MS } from './constants';
6
+import { FACE_BOX_EVENT_TYPE, FACE_LANDMARKS_EVENT_TYPE, SEND_IMAGE_INTERVAL_MS } from './constants';
6 7
 import logger from './logger';
7
-import { FaceBox } from './types';
8
-
9
-let canvas: HTMLCanvasElement;
10
-let context: CanvasRenderingContext2D | null;
11
-
12
-if (typeof OffscreenCanvas === 'undefined') {
13
-    canvas = document.createElement('canvas');
14
-    context = canvas.getContext('2d');
15
-}
8
+import { FaceBox, FaceLandmarks } from './types';
16 9
 
17 10
 /**
18
- * Sends the face expression with its duration to all the other participants.
11
+ * Sends the face landmarks to other participants via the data channel.
19 12
  *
20 13
  * @param {any} conference - The current conference.
21
- * @param  {string} faceExpression - Face expression to be sent.
22
- * @param {number} duration - The duration of the face expression in seconds.
14
+ * @param  {FaceLandmarks} faceLandmarks - Face landmarks to be sent.
23 15
  * @returns {void}
24 16
  */
25
-export function sendFaceExpressionToParticipants(
26
-        conference: any,
27
-        faceExpression: string,
28
-        duration: number
29
-): void {
17
+export function sendFaceExpressionToParticipants(conference: any, faceLandmarks: FaceLandmarks): void {
30 18
     try {
31 19
         conference.sendEndpointMessage('', {
32
-            type: 'face_landmark',
33
-            faceExpression,
34
-            duration
20
+            type: FACE_LANDMARKS_EVENT_TYPE,
21
+            faceLandmarks
35 22
         });
36 23
     } catch (err) {
37
-        logger.warn('Could not broadcast the face expression to the other participants', err);
24
+        logger.warn('Could not broadcast the face landmarks to the other participants', err);
38 25
     }
39 26
 
40 27
 }
@@ -61,30 +48,22 @@ export function sendFaceBoxToParticipants(
61 48
 }
62 49
 
63 50
 /**
64
- * Sends the face expression with its duration to xmpp server.
51
+ * Sends the face landmarks to prosody.
65 52
  *
66 53
  * @param {any} conference - The current conference.
67
- * @param  {string} faceExpression - Face expression to be sent.
68
- * @param {number} duration - The duration of the face expression in seconds.
54
+ * @param  {FaceLandmarks} faceLandmarks - Face landmarks to be sent.
69 55
  * @returns {void}
70 56
  */
71
-export function sendFaceExpressionToServer(
72
-        conference: any,
73
-        faceExpression: string,
74
-        duration: number
75
-): void {
57
+export function sendFaceExpressionToServer(conference: IJitsiConference, faceLandmarks: FaceLandmarks): void {
76 58
     try {
77
-        conference.sendFaceLandmarks({
78
-            faceExpression,
79
-            duration
80
-        });
59
+        conference.sendFaceLandmarks(faceLandmarks);
81 60
     } catch (err) {
82
-        logger.warn('Could not send the face expression to xmpp server', err);
61
+        logger.warn('Could not send the face landmarks to prosody', err);
83 62
     }
84 63
 }
85 64
 
86 65
 /**
87
- * Sends face expression to backend.
66
+ * Sends face landmarks to backend.
88 67
  *
89 68
  * @param  {Object} state - Redux state.
90 69
  * @returns {boolean} - True if sent, false otherwise.
@@ -96,9 +75,9 @@ export async function sendFaceExpressionsWebhook(state: IReduxState) {
96 75
     const { connection } = state['features/base/connection'];
97 76
     const jid = connection?.getJid();
98 77
     const localParticipant = getLocalParticipant(state);
99
-    const { faceExpressionsBuffer } = state['features/face-landmarks'];
78
+    const { faceLandmarksBuffer } = state['features/face-landmarks'];
100 79
 
101
-    if (faceExpressionsBuffer.length === 0) {
80
+    if (faceLandmarksBuffer.length === 0) {
102 81
         return false;
103 82
     }
104 83
 
@@ -111,7 +90,7 @@ export async function sendFaceExpressionsWebhook(state: IReduxState) {
111 90
         meetingFqn: extractFqnFromPath(),
112 91
         sessionId: conference?.sessionId,
113 92
         submitted: Date.now(),
114
-        emotions: faceExpressionsBuffer,
93
+        emotions: faceLandmarksBuffer,
115 94
         participantId: localParticipant?.jwtId,
116 95
         participantName: localParticipant?.name,
117 96
         participantJid: jid
@@ -138,55 +117,6 @@ export async function sendFaceExpressionsWebhook(state: IReduxState) {
138 117
 }
139 118
 
140 119
 
141
-/**
142
- * Sends the image data a canvas from the track in the image capture to the face recognition worker.
143
- *
144
- * @param {Worker} worker - Face recognition worker.
145
- * @param {Object} imageCapture - Image capture that contains the current track.
146
- * @param {number} threshold - Movement threshold as percentage for sharing face coordinates.
147
- * @returns {Promise<boolean>} - True if sent, false otherwise.
148
- */
149
-export async function sendDataToWorker(
150
-        worker: Worker,
151
-        imageCapture: ImageCapture,
152
-        threshold = 10
153
-): Promise<boolean> {
154
-    if (imageCapture === null || imageCapture === undefined) {
155
-        return false;
156
-    }
157
-
158
-    let imageBitmap;
159
-    let image;
160
-
161
-    try {
162
-        imageBitmap = await imageCapture.grabFrame();
163
-    } catch (err) {
164
-        logger.warn(err);
165
-
166
-        return false;
167
-    }
168
-
169
-    if (typeof OffscreenCanvas === 'undefined') {
170
-        canvas.width = imageBitmap.width;
171
-        canvas.height = imageBitmap.height;
172
-        context?.drawImage(imageBitmap, 0, 0);
173
-
174
-        image = context?.getImageData(0, 0, imageBitmap.width, imageBitmap.height);
175
-    } else {
176
-        image = imageBitmap;
177
-    }
178
-
179
-    worker.postMessage({
180
-        type: DETECT_FACE,
181
-        image,
182
-        threshold
183
-    });
184
-
185
-    imageBitmap.close();
186
-
187
-    return true;
188
-}
189
-
190 120
 /**
191 121
  * Gets face box for a participant id.
192 122
  *
@@ -230,14 +160,3 @@ export function getDetectionInterval(state: IReduxState) {
230 160
 
231 161
     return Math.max(faceLandmarks?.captureInterval || SEND_IMAGE_INTERVAL_MS);
232 162
 }
233
-
234
-/**
235
- * Returns the duration in seconds of a face expression.
236
- *
237
- * @param {IReduxState} state - The redux state.
238
- * @param {number} faceExpressionCount - The number of consecutive face expressions.
239
- * @returns {number} - Duration of face expression in seconds.
240
- */
241
-export function getFaceExpressionDuration(state: IReduxState, faceExpressionCount: number) {
242
-    return faceExpressionCount * (getDetectionInterval(state) / 1000);
243
-}

+ 8
- 14
react/features/face-landmarks/middleware.ts Parādīt failu

@@ -11,18 +11,15 @@ import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
11 11
 import { TRACK_ADDED, TRACK_REMOVED, TRACK_UPDATED } from '../base/tracks/actionTypes';
12 12
 
13 13
 import FaceLandmarksDetector from './FaceLandmarksDetector';
14
-import { ADD_FACE_EXPRESSION, NEW_FACE_COORDINATES, UPDATE_FACE_COORDINATES } from './actionTypes';
15
-import {
16
-    addToFaceExpressionsBuffer
17
-} from './actions';
14
+import { ADD_FACE_LANDMARKS, NEW_FACE_COORDINATES, UPDATE_FACE_COORDINATES } from './actionTypes';
18 15
 import { FACE_BOX_EVENT_TYPE } from './constants';
19 16
 import { sendFaceBoxToParticipants, sendFaceExpressionToParticipants, sendFaceExpressionToServer } from './functions';
20 17
 
21 18
 
22 19
 MiddlewareRegistry.register((store: IStore) => (next: Function) => (action: any) => {
23 20
     const { dispatch, getState } = store;
24
-    const { faceLandmarks } = getState()['features/base/config'];
25
-    const isEnabled = faceLandmarks?.enableFaceCentering || faceLandmarks?.enableFaceExpressionsDetection;
21
+    const { faceLandmarks: faceLandmarksConfig } = getState()['features/base/config'];
22
+    const isEnabled = faceLandmarksConfig?.enableFaceCentering || faceLandmarksConfig?.enableFaceExpressionsDetection;
26 23
 
27 24
     if (action.type === CONFERENCE_JOINED) {
28 25
         if (isEnabled) {
@@ -99,19 +96,16 @@ MiddlewareRegistry.register((store: IStore) => (next: Function) => (action: any)
99 96
 
100 97
         return next(action);
101 98
     }
102
-    case ADD_FACE_EXPRESSION: {
99
+    case ADD_FACE_LANDMARKS: {
103 100
         const state = getState();
104
-        const { faceExpression, duration, timestamp } = action;
101
+        const { faceLandmarks } = action;
105 102
         const conference = getCurrentConference(state);
106 103
 
107 104
         if (getParticipantCount(state) > 1) {
108
-            sendFaceExpressionToParticipants(conference, faceExpression, duration);
105
+            sendFaceExpressionToParticipants(conference, faceLandmarks);
109 106
         }
110
-        sendFaceExpressionToServer(conference, faceExpression, duration);
111
-        dispatch(addToFaceExpressionsBuffer({
112
-            emotion: faceExpression,
113
-            timestamp
114
-        }));
107
+
108
+        sendFaceExpressionToServer(conference, faceLandmarks);
115 109
 
116 110
         return next(action);
117 111
     }

+ 19
- 39
react/features/face-landmarks/reducer.ts Parādīt failu

@@ -1,42 +1,25 @@
1 1
 import ReducerRegistry from '../base/redux/ReducerRegistry';
2 2
 
3 3
 import {
4
-    ADD_FACE_EXPRESSION,
5
-    ADD_TO_FACE_EXPRESSIONS_BUFFER,
6
-    CLEAR_FACE_EXPRESSIONS_BUFFER,
4
+    ADD_FACE_LANDMARKS,
5
+    CLEAR_FACE_LANDMARKS_BUFFER,
7 6
     UPDATE_FACE_COORDINATES
8 7
 } from './actionTypes';
9
-import { FaceBox } from './types';
8
+import { FaceBox, FaceLandmarks } from './types';
10 9
 
11 10
 const defaultState = {
12 11
     faceBoxes: {},
13
-    faceExpressions: {
14
-        happy: 0,
15
-        neutral: 0,
16
-        surprised: 0,
17
-        angry: 0,
18
-        fearful: 0,
19
-        disgusted: 0,
20
-        sad: 0
21
-    },
22
-    faceExpressionsBuffer: [],
12
+    faceLandmarks: [],
13
+    faceLandmarksBuffer: [],
23 14
     recognitionActive: false
24 15
 };
25 16
 
26 17
 export interface IFaceLandmarksState {
27 18
     faceBoxes: { [key: string]: FaceBox; };
28
-    faceExpressions: {
29
-        angry: number;
30
-        disgusted: number;
31
-        fearful: number;
32
-        happy: number;
33
-        neutral: number;
34
-        sad: number;
35
-        surprised: number;
36
-    };
37
-    faceExpressionsBuffer: Array<{
19
+    faceLandmarks: Array<FaceLandmarks>;
20
+    faceLandmarksBuffer: Array<{
38 21
         emotion: string;
39
-        timestamp: string;
22
+        timestamp: number;
40 23
     }>;
41 24
     recognitionActive: boolean;
42 25
 }
@@ -44,26 +27,23 @@ export interface IFaceLandmarksState {
44 27
 ReducerRegistry.register<IFaceLandmarksState>('features/face-landmarks',
45 28
 (state = defaultState, action): IFaceLandmarksState => {
46 29
     switch (action.type) {
47
-    case ADD_FACE_EXPRESSION: {
48
-        return {
49
-            ...state,
50
-            faceExpressions: {
51
-                ...state.faceExpressions,
52
-                [action.faceExpression]: state.faceExpressions[
53
-                    action.faceExpression as keyof typeof state.faceExpressions] + action.duration
54
-            }
55
-        };
56
-    }
57
-    case ADD_TO_FACE_EXPRESSIONS_BUFFER: {
30
+    case ADD_FACE_LANDMARKS: {
31
+        const { addToBuffer, faceLandmarks }: { addToBuffer: boolean; faceLandmarks: FaceLandmarks; } = action;
32
+
58 33
         return {
59 34
             ...state,
60
-            faceExpressionsBuffer: [ ...state.faceExpressionsBuffer, action.faceExpression ]
35
+            faceLandmarks: [ ...state.faceLandmarks, faceLandmarks ],
36
+            faceLandmarksBuffer: addToBuffer ? [ ...state.faceLandmarksBuffer,
37
+                {
38
+                    emotion: faceLandmarks.faceExpression,
39
+                    timestamp: faceLandmarks.timestamp
40
+                } ] : state.faceLandmarksBuffer
61 41
         };
62 42
     }
63
-    case CLEAR_FACE_EXPRESSIONS_BUFFER: {
43
+    case CLEAR_FACE_LANDMARKS_BUFFER: {
64 44
         return {
65 45
             ...state,
66
-            faceExpressionsBuffer: []
46
+            faceLandmarksBuffer: []
67 47
         };
68 48
     }
69 49
     case UPDATE_FACE_COORDINATES: {

+ 17
- 1
react/features/face-landmarks/types.ts Parādīt failu

@@ -19,5 +19,21 @@ export type InitInput = {
19 19
 export type DetectOutput = {
20 20
     faceBox?: FaceBox;
21 21
     faceCount: number;
22
-    faceExpression?: string;
22
+    faceExpression?: FaceExpression;
23
+};
24
+
25
+export type FaceExpression = {
26
+    expression: string;
27
+    score: number;
28
+};
29
+
30
+export type FaceLandmarks = {
31
+
32
+    // duration in milliseconds of the face landmarks
33
+    duration: number;
34
+    faceExpression: string;
35
+    score?: number;
36
+
37
+    // the start timestamp of the expression
38
+    timestamp: number;
23 39
 };

+ 7
- 4
react/features/rtcstats/middleware.ts Parādīt failu

@@ -14,7 +14,8 @@ import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
14 14
 import { TRACK_ADDED, TRACK_UPDATED } from '../base/tracks/actionTypes';
15 15
 import { getCurrentRoomId, isInBreakoutRoom } from '../breakout-rooms/functions';
16 16
 import { extractFqnFromPath } from '../dynamic-branding/functions.any';
17
-import { ADD_FACE_EXPRESSION } from '../face-landmarks/actionTypes';
17
+import { ADD_FACE_LANDMARKS } from '../face-landmarks/actionTypes';
18
+import { FaceLandmarks } from '../face-landmarks/types';
18 19
 
19 20
 import RTCStats from './RTCStats';
20 21
 import {
@@ -164,17 +165,19 @@ MiddlewareRegistry.register((store: IStore) => (next: Function) => (action: AnyA
164 165
         }
165 166
         break;
166 167
     }
167
-    case ADD_FACE_EXPRESSION:
168
+    case ADD_FACE_LANDMARKS: {
168 169
         if (canSendFaceLandmarksRtcstatsData(state)) {
169
-            const { duration, faceExpression, timestamp } = action;
170
+            const { duration, faceExpression, timestamp } = action.faceLandmarks as FaceLandmarks;
171
+            const durationSeconds = Math.round(duration / 1000);
170 172
 
171 173
             RTCStats.sendFaceLandmarksData({
172
-                duration,
174
+                duration: durationSeconds,
173 175
                 faceLandmarks: faceExpression,
174 176
                 timestamp
175 177
             });
176 178
         }
177 179
         break;
180
+    }
178 181
     case CONFERENCE_TIMESTAMP_CHANGED: {
179 182
         if (canSendRtcstatsData(state)) {
180 183
             const { conferenceTimestamp } = action;

+ 17
- 0
react/features/speaker-stats/actionTypes.ts Parādīt failu

@@ -63,3 +63,20 @@ export const RESET_SEARCH_CRITERIA = 'RESET_SEARCH_CRITERIA'
63 63
  */
64 64
 export const TOGGLE_FACE_EXPRESSIONS = 'SHOW_FACE_EXPRESSIONS';
65 65
 
66
+
67
+export const INCREASE_ZOOM = 'INCREASE_ZOOM';
68
+
69
+export const DECREASE_ZOOM = 'DECREASE_ZOOM';
70
+
71
+export const ADD_TO_OFFSET = 'ADD_TO_OFFSET';
72
+
73
+export const SET_OFFSET = 'RESET_OFFSET';
74
+
75
+export const ADD_TO_OFFSET_LEFT = 'ADD_TO_OFFSET_LEFT';
76
+
77
+export const ADD_TO_OFFSET_RIGHT = 'ADD_TO_OFFSET_RIGHT';
78
+
79
+export const SET_TIMELINE_BOUNDARY = 'SET_TIMELINE_BOUNDARY';
80
+
81
+export const SET_PANNING = 'SET_PANNING';
82
+

+ 231
- 0
react/features/speaker-stats/actions.any.ts Parādīt failu

@@ -0,0 +1,231 @@
1
+import { IStore } from '../app/types';
2
+
3
+import {
4
+    ADD_TO_OFFSET,
5
+    ADD_TO_OFFSET_LEFT,
6
+    ADD_TO_OFFSET_RIGHT,
7
+    INIT_REORDER_STATS,
8
+    INIT_SEARCH,
9
+    INIT_UPDATE_STATS,
10
+    RESET_SEARCH_CRITERIA,
11
+    SET_PANNING,
12
+    SET_TIMELINE_BOUNDARY,
13
+    TOGGLE_FACE_EXPRESSIONS,
14
+    UPDATE_SORTED_SPEAKER_STATS_IDS,
15
+    UPDATE_STATS
16
+} from './actionTypes';
17
+import { MINIMUM_INTERVAL } from './constants';
18
+import { getCurrentDuration, getTimelineBoundaries } from './functions';
19
+import { ISpeakerStats } from './reducer';
20
+
21
+/**
22
+ * Starts a search by criteria.
23
+ *
24
+ * @param {string} criteria - The search criteria.
25
+ * @returns {Object}
26
+ */
27
+export function initSearch(criteria: string) {
28
+    return {
29
+        type: INIT_SEARCH,
30
+        criteria
31
+    };
32
+}
33
+
34
+/**
35
+ * Gets the new stats and triggers update.
36
+ *
37
+ * @param {Function} getSpeakerStats - Function to get the speaker stats.
38
+ * @returns {Object}
39
+ */
40
+export function initUpdateStats(getSpeakerStats: () => ISpeakerStats) {
41
+    return {
42
+        type: INIT_UPDATE_STATS,
43
+        getSpeakerStats
44
+    };
45
+}
46
+
47
+/**
48
+ * Updates the stats with new stats.
49
+ *
50
+ * @param {Object} stats - The new stats.
51
+ * @returns {Object}
52
+ */
53
+export function updateStats(stats: Object) {
54
+    return {
55
+        type: UPDATE_STATS,
56
+        stats
57
+    };
58
+}
59
+
60
+/**
61
+ * Updates the speaker stats order.
62
+ *
63
+ * @param {Array<string>} participantIds - Participant ids.
64
+ * @returns {Object}
65
+ */
66
+export function updateSortedSpeakerStatsIds(participantIds: Array<string>) {
67
+    return {
68
+        type: UPDATE_SORTED_SPEAKER_STATS_IDS,
69
+        participantIds
70
+    };
71
+}
72
+
73
+/**
74
+ * Initiates reordering of the stats.
75
+ *
76
+ * @returns {Object}
77
+ */
78
+export function initReorderStats() {
79
+    return {
80
+        type: INIT_REORDER_STATS
81
+    };
82
+}
83
+
84
+/**
85
+ * Resets the search criteria.
86
+ *
87
+ * @returns {Object}
88
+ */
89
+export function resetSearchCriteria() {
90
+    return {
91
+        type: RESET_SEARCH_CRITERIA
92
+    };
93
+}
94
+
95
+/**
96
+ * Toggles the face expressions grid.
97
+ *
98
+ * @returns {Object}
99
+ */
100
+export function toggleFaceExpressions() {
101
+    return {
102
+        type: TOGGLE_FACE_EXPRESSIONS
103
+    };
104
+}
105
+
106
+/**
107
+ * Adds a value to the boundary offset of the timeline.
108
+ *
109
+ * @param {number} value - The value to be added.
110
+ * @param {number} left - The left boundary.
111
+ * @param {number} right - The right boundary.
112
+ * @param {number} currentDuration - The currentDuration of the conference.
113
+ * @returns {Object}
114
+ */
115
+export function addToOffset(value: number) {
116
+    return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
117
+        const state = getState();
118
+        const { left, right } = getTimelineBoundaries(state);
119
+        const currentDuration = getCurrentDuration(state) ?? 0;
120
+        const newLeft = left + value;
121
+        const newRight = right + value;
122
+
123
+        if (newLeft >= 0 && newRight <= currentDuration) {
124
+            dispatch({
125
+                type: ADD_TO_OFFSET,
126
+                value
127
+            });
128
+        } else if (newLeft < 0) {
129
+            dispatch({
130
+                type: ADD_TO_OFFSET,
131
+                value: -left
132
+            });
133
+        } else if (newRight > currentDuration) {
134
+            dispatch({
135
+                type: ADD_TO_OFFSET,
136
+                value: currentDuration - right
137
+            });
138
+        }
139
+    };
140
+}
141
+
142
+/**
143
+ * Adds the value to the offset of the left boundary for the timeline.
144
+ *
145
+ * @param {number} value - The new value for the offset.
146
+ * @returns {Object}
147
+ */
148
+export function addToOffsetLeft(value: number) {
149
+    return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
150
+        const state = getState();
151
+        const { left, right } = getTimelineBoundaries(state);
152
+        const newLeft = left + value;
153
+
154
+        if (newLeft >= 0 && right - newLeft > MINIMUM_INTERVAL) {
155
+            dispatch({
156
+                type: ADD_TO_OFFSET_LEFT,
157
+                value
158
+            });
159
+        } else if (newLeft < 0) {
160
+            dispatch({
161
+                type: ADD_TO_OFFSET_LEFT,
162
+                value: -left
163
+            });
164
+        }
165
+    };
166
+}
167
+
168
+/**
169
+ * Adds the value to the offset of the right boundary for the timeline.
170
+ *
171
+ * @param {number} value - The new value for the offset.
172
+ * @returns {Object}
173
+ */
174
+export function addToOffsetRight(value: number) {
175
+    return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
176
+        const state = getState();
177
+        const { left, right } = getTimelineBoundaries(state);
178
+        const currentDuration = getCurrentDuration(state) ?? 0;
179
+        const newRight = right + value;
180
+
181
+        if (newRight <= currentDuration && newRight - left > MINIMUM_INTERVAL) {
182
+            dispatch({
183
+                type: ADD_TO_OFFSET_RIGHT,
184
+                value
185
+            });
186
+        } else if (newRight > currentDuration) {
187
+            dispatch({
188
+                type: ADD_TO_OFFSET_RIGHT,
189
+                value: currentDuration - right
190
+            });
191
+        }
192
+    };
193
+}
194
+
195
+/**
196
+ * Sets the current time boundary of the timeline, when zoomed in.
197
+ *
198
+ * @param {number} boundary - The current time boundary.
199
+ * @returns {Object}
200
+ */
201
+export function setTimelineBoundary(boundary: number) {
202
+    return {
203
+        type: SET_TIMELINE_BOUNDARY,
204
+        boundary
205
+    };
206
+}
207
+
208
+/**
209
+ * Clears the current time boundary of the timeline, when zoomed out full.
210
+ *
211
+ * @returns {Object}
212
+ */
213
+export function clearTimelineBoundary() {
214
+    return {
215
+        type: SET_TIMELINE_BOUNDARY,
216
+        boundary: null
217
+    };
218
+}
219
+
220
+/**
221
+ * Sets the state of the timeline panning.
222
+ *
223
+ * @param {Object} panning - The state of the timeline panning.
224
+ * @returns {Object}
225
+ */
226
+export function setTimelinePanning(panning: { active: boolean; x: number; }) {
227
+    return {
228
+        type: SET_PANNING,
229
+        panning
230
+    };
231
+}

+ 1
- 0
react/features/speaker-stats/actions.native.ts Parādīt failu

@@ -0,0 +1 @@
1
+export * from './actions.any';

+ 0
- 94
react/features/speaker-stats/actions.ts Parādīt failu

@@ -1,94 +0,0 @@
1
-import {
2
-    INIT_REORDER_STATS,
3
-    INIT_SEARCH,
4
-    INIT_UPDATE_STATS,
5
-    RESET_SEARCH_CRITERIA,
6
-    TOGGLE_FACE_EXPRESSIONS,
7
-    UPDATE_SORTED_SPEAKER_STATS_IDS,
8
-    UPDATE_STATS
9
-} from './actionTypes';
10
-
11
-/**
12
- * Starts a search by criteria.
13
- *
14
- * @param {string | null} criteria - The search criteria.
15
- * @returns {Object}
16
- */
17
-export function initSearch(criteria: string | null) {
18
-    return {
19
-        type: INIT_SEARCH,
20
-        criteria
21
-    };
22
-}
23
-
24
-/**
25
- * Gets the new stats and triggers update.
26
- *
27
- * @param {Function} getSpeakerStats - Function to get the speaker stats.
28
- * @returns {Object}
29
- */
30
-export function initUpdateStats(getSpeakerStats: Function) {
31
-    return {
32
-        type: INIT_UPDATE_STATS,
33
-        getSpeakerStats
34
-    };
35
-}
36
-
37
-/**
38
- * Updates the stats with new stats.
39
- *
40
- * @param {Object} stats - The new stats.
41
- * @returns {Object}
42
- */
43
-export function updateStats(stats: Object) {
44
-    return {
45
-        type: UPDATE_STATS,
46
-        stats
47
-    };
48
-}
49
-
50
-/**
51
- * Updates the speaker stats order.
52
- *
53
- * @param {Object} participantIds - Participant ids.
54
- * @returns {Object}
55
- */
56
-export function updateSortedSpeakerStatsIds(participantIds?: Array<string>) {
57
-    return {
58
-        type: UPDATE_SORTED_SPEAKER_STATS_IDS,
59
-        participantIds
60
-    };
61
-}
62
-
63
-/**
64
- * Initiates reordering of the stats.
65
- *
66
- * @returns {Object}
67
- */
68
-export function initReorderStats() {
69
-    return {
70
-        type: INIT_REORDER_STATS
71
-    };
72
-}
73
-
74
-/**
75
- * Resets the search criteria.
76
- *
77
- * @returns {Object}
78
- */
79
-export function resetSearchCriteria() {
80
-    return {
81
-        type: RESET_SEARCH_CRITERIA
82
-    };
83
-}
84
-
85
-/**
86
- * Toggles the face expressions grid.
87
- *
88
- * @returns {Object}
89
- */
90
-export function toggleFaceExpressions() {
91
-    return {
92
-        type: TOGGLE_FACE_EXPRESSIONS
93
-    };
94
-}

+ 1
- 0
react/features/speaker-stats/actions.web.ts Parādīt failu

@@ -0,0 +1 @@
1
+export * from './actions.any';

react/features/speaker-stats/components/AbstractSpeakerStatsButton.js → react/features/speaker-stats/components/AbstractSpeakerStatsButton.tsx Parādīt failu

@@ -1,24 +1,22 @@
1
-// @flow
2
-
3
-import type { Dispatch } from 'redux';
4
-
5
-import { IconConnection } from '../../base/icons';
6
-import { AbstractButton } from '../../base/toolbox/components';
7
-import type { AbstractButtonProps } from '../../base/toolbox/components';
1
+import { IStore } from '../../app/types';
2
+import { IconConnection } from '../../base/icons/svg';
3
+// eslint-disable-next-line lines-around-comment
4
+// @ts-ignore
5
+import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components';
8 6
 
9 7
 type Props = AbstractButtonProps & {
10 8
 
11 9
     /**
12 10
      * True if the navigation bar should be visible.
13 11
      */
14
-    dispatch: Dispatch<any>
12
+    dispatch: IStore['dispatch'];
15 13
 };
16 14
 
17 15
 
18 16
 /**
19 17
  * Implementation of a button for opening speaker stats dialog.
20 18
  */
21
-class AbstractSpeakerStatsButton extends AbstractButton<Props, *> {
19
+class AbstractSpeakerStatsButton extends AbstractButton<Props, any, any> {
22 20
     accessibilityLabel = 'toolbar.accessibilityLabel.speakerStats';
23 21
     icon = IconConnection;
24 22
     label = 'toolbar.speakerStats';

react/features/speaker-stats/components/AbstractSpeakerStatsList.js → react/features/speaker-stats/components/AbstractSpeakerStatsList.ts Parādīt failu

@@ -1,11 +1,10 @@
1
-// @flow
2
-
3 1
 import { useCallback, useEffect, useRef } from 'react';
4 2
 import { useTranslation } from 'react-i18next';
5 3
 import { useDispatch, useSelector } from 'react-redux';
6 4
 
7
-import { getLocalParticipant } from '../../base/participants';
8
-import { initUpdateStats } from '../actions';
5
+import { IReduxState } from '../../app/types';
6
+import { getLocalParticipant } from '../../base/participants/functions';
7
+import { initUpdateStats } from '../actions.any';
9 8
 import {
10 9
     SPEAKER_STATS_RELOAD_INTERVAL
11 10
 } from '../constants';
@@ -17,21 +16,22 @@ import {
17 16
  * @param {Object} itemStyles - Styles for the speaker stats item.
18 17
  * @returns {Function}
19 18
  */
20
-const abstractSpeakerStatsList = (speakerStatsItem: Function, itemStyles?: Object): Function[] => {
19
+const abstractSpeakerStatsList = (speakerStatsItem: Function): Function[] => {
21 20
     const dispatch = useDispatch();
22 21
     const { t } = useTranslation();
23
-    const conference = useSelector(state => state['features/base/conference'].conference);
22
+    const { conference } = useSelector((state: IReduxState) => state['features/base/conference']);
24 23
     const {
25 24
         stats: speakerStats,
26 25
         showFaceExpressions,
27 26
         sortedSpeakerStatsIds
28
-    } = useSelector(state => state['features/speaker-stats']);
27
+    } = useSelector((state: IReduxState) => state['features/speaker-stats']);
29 28
     const localParticipant = useSelector(getLocalParticipant);
30 29
     const { defaultRemoteDisplayName } = useSelector(
31
-        state => state['features/base/config']) || {};
32
-    const { faceLandmarks } = useSelector(state => state['features/base/config']) || {};
33
-    const { faceExpressions } = useSelector(state => state['features/face-landmarks']) || {};
34
-    const reloadInterval = useRef(null);
30
+        (state: IReduxState) => state['features/base/config']) || {};
31
+    const { faceLandmarks: faceLandmarksConfig } = useSelector((state: IReduxState) =>
32
+        state['features/base/config']) || {};
33
+    const { faceLandmarks } = useSelector((state: IReduxState) => state['features/face-landmarks']) || {};
34
+    const reloadInterval = useRef<number>();
35 35
 
36 36
     /**
37 37
      * Update the internal state with the latest speaker stats.
@@ -40,7 +40,7 @@ const abstractSpeakerStatsList = (speakerStatsItem: Function, itemStyles?: Objec
40 40
      * @private
41 41
      */
42 42
     const getSpeakerStats = useCallback(() => {
43
-        const stats = conference.getSpeakerStats();
43
+        const stats = conference?.getSpeakerStats();
44 44
 
45 45
         for (const userId in stats) {
46 46
             if (stats[userId]) {
@@ -48,40 +48,42 @@ const abstractSpeakerStatsList = (speakerStatsItem: Function, itemStyles?: Objec
48 48
                     const meString = t('me');
49 49
 
50 50
                     stats[userId].setDisplayName(
51
-                        localParticipant.name
51
+                        localParticipant?.name
52 52
                             ? `${localParticipant.name} (${meString})`
53 53
                             : meString
54 54
                     );
55
-                    if (faceLandmarks?.enableDisplayFaceExpressions) {
56
-                        stats[userId].setFaceExpressions(faceExpressions);
55
+
56
+                    if (faceLandmarksConfig?.enableDisplayFaceExpressions) {
57
+                        stats[userId].setFaceLandmarks(faceLandmarks);
57 58
                     }
58 59
                 }
59 60
 
60 61
                 if (!stats[userId].getDisplayName()) {
61 62
                     stats[userId].setDisplayName(
62
-                        conference.getParticipantById(userId)?.name
63
+                        conference?.getParticipantById(userId)?.name
63 64
                     );
64 65
                 }
65 66
             }
66 67
         }
67 68
 
68
-        return stats;
69
-    }, [ faceExpressions ]);
69
+        return stats ?? {};
70
+    }, [ faceLandmarks ]);
70 71
 
71 72
     const updateStats = useCallback(
72 73
         () => dispatch(initUpdateStats(getSpeakerStats)),
73 74
     [ dispatch, initUpdateStats, getSpeakerStats ]);
74 75
 
75 76
     useEffect(() => {
76
-        if (reloadInterval.current) {
77
-            clearInterval(reloadInterval.current);
78
-        }
79
-        reloadInterval.current = setInterval(() => {
77
+        reloadInterval.current = window.setInterval(() => {
80 78
             updateStats();
81 79
         }, SPEAKER_STATS_RELOAD_INTERVAL);
82 80
 
83
-        return () => clearInterval(reloadInterval.current);
84
-    }, [ faceExpressions ]);
81
+        return () => {
82
+            if (reloadInterval.current) {
83
+                clearInterval(reloadInterval.current);
84
+            }
85
+        };
86
+    }, [ faceLandmarks ]);
85 87
 
86 88
     const localSpeakerStats = Object.keys(speakerStats).length === 0 ? getSpeakerStats() : speakerStats;
87 89
     const localSortedSpeakerStatsIds
@@ -91,22 +93,17 @@ const abstractSpeakerStatsList = (speakerStatsItem: Function, itemStyles?: Objec
91 93
 
92 94
     return userIds.map(userId => {
93 95
         const statsModel = localSpeakerStats[userId];
94
-        const props = {};
95
-
96
-        props.isDominantSpeaker = statsModel.isDominantSpeaker();
97
-        props.dominantSpeakerTime = statsModel.getTotalDominantSpeakerTime();
98
-        props.participantId = userId;
99
-        props.hasLeft = statsModel.hasLeft();
100
-        if (showFaceExpressions) {
101
-            props.faceExpressions = statsModel.getFaceExpressions();
102
-        }
103
-        props.hidden = statsModel.hidden;
104
-        props.showFaceExpressions = showFaceExpressions;
105
-        props.displayName = statsModel.getDisplayName() || defaultRemoteDisplayName;
106
-        if (itemStyles) {
107
-            props.styles = itemStyles;
108
-        }
109
-        props.t = t;
96
+        const props = {
97
+            isDominantSpeaker: statsModel.isDominantSpeaker(),
98
+            dominantSpeakerTime: statsModel.getTotalDominantSpeakerTime(),
99
+            participantId: userId,
100
+            hasLeft: statsModel.hasLeft(),
101
+            faceLandmarks: showFaceExpressions ? statsModel.getFaceLandmarks() : undefined,
102
+            hidden: statsModel.hidden,
103
+            showFaceExpressions,
104
+            displayName: statsModel.getDisplayName() || defaultRemoteDisplayName,
105
+            t
106
+        };
110 107
 
111 108
         return speakerStatsItem(props);
112 109
     });

react/features/speaker-stats/components/_.native.js → react/features/speaker-stats/components/_.native.ts Parādīt failu

@@ -1 +1,2 @@
1
+// @ts-ignore
1 2
 export * from './native';

react/features/speaker-stats/components/_.web.js → react/features/speaker-stats/components/_.web.ts Parādīt failu


react/features/speaker-stats/components/index.js → react/features/speaker-stats/components/index.ts Parādīt failu

@@ -1 +1,2 @@
1
+// @ts-ignore
1 2
 export * from './_';

react/features/speaker-stats/components/timeFunctions.js → react/features/speaker-stats/components/timeFunctions.ts Parādīt failu

@@ -7,7 +7,7 @@
7 7
  * @private
8 8
  * @returns {number}
9 9
  */
10
-function getHoursCount(milliseconds) {
10
+function getHoursCount(milliseconds: number) {
11 11
     return Math.floor(milliseconds / (60 * 60 * 1000));
12 12
 }
13 13
 
@@ -18,7 +18,7 @@ function getHoursCount(milliseconds) {
18 18
  * @private
19 19
  * @returns {number}
20 20
  */
21
-function getMinutesCount(milliseconds) {
21
+function getMinutesCount(milliseconds: number) {
22 22
     return Math.floor(milliseconds / (60 * 1000) % 60);
23 23
 }
24 24
 
@@ -29,7 +29,7 @@ function getMinutesCount(milliseconds) {
29 29
  * @private
30 30
  * @returns {number}
31 31
  */
32
-function getSecondsCount(milliseconds) {
32
+function getSecondsCount(milliseconds: number) {
33 33
     return Math.floor(milliseconds / 1000 % 60);
34 34
 }
35 35
 
@@ -85,6 +85,6 @@ export function createLocalizedTime(time: number, t: Function) {
85 85
  * key for react to iterate upon.
86 86
  * @returns {string}
87 87
  */
88
-function createTimeDisplay(count, countNounKey, t) {
88
+function createTimeDisplay(count: number, countNounKey: string, t: Function) {
89 89
     return t(countNounKey, { count });
90 90
 }

+ 205
- 73
react/features/speaker-stats/components/web/SpeakerStats.tsx Parādīt failu

@@ -1,15 +1,28 @@
1 1
 import React, { useCallback, useEffect } from 'react';
2
+import { useTranslation } from 'react-i18next';
2 3
 import { useDispatch, useSelector } from 'react-redux';
3 4
 import { makeStyles } from 'tss-react/mui';
4 5
 
5 6
 import { IReduxState } from '../../../app/types';
7
+import Icon from '../../../base/icons/components/Icon';
8
+import {
9
+    IconEmotionsAngry,
10
+    IconEmotionsDisgusted,
11
+    IconEmotionsFearful,
12
+    IconEmotionsHappy,
13
+    IconEmotionsNeutral,
14
+    IconEmotionsSad,
15
+    IconEmotionsSurprised
16
+} from '../../../base/icons/svg';
17
+// eslint-disable-next-line lines-around-comment
18
+// @ts-ignore
19
+import { Tooltip } from '../../../base/tooltip';
6 20
 import Dialog from '../../../base/ui/components/web/Dialog';
7 21
 import { escapeRegexp } from '../../../base/util/helpers';
8
-import { initSearch, resetSearchCriteria, toggleFaceExpressions } from '../../actions';
22
+import { initSearch, resetSearchCriteria, toggleFaceExpressions } from '../../actions.any';
9 23
 import {
10 24
     DISPLAY_SWITCH_BREAKPOINT,
11
-    MOBILE_BREAKPOINT,
12
-    RESIZE_SEARCH_SWITCH_CONTAINER_BREAKPOINT
25
+    MOBILE_BREAKPOINT
13 26
 } from '../../constants';
14 27
 
15 28
 import FaceExpressionsSwitch from './FaceExpressionsSwitch';
@@ -20,69 +33,171 @@ import SpeakerStatsSearch from './SpeakerStatsSearch';
20 33
 const useStyles = makeStyles()(theme => {
21 34
     return {
22 35
         speakerStats: {
36
+            '& .header': {
37
+                position: 'fixed',
38
+                backgroundColor: theme.palette.ui01,
39
+                paddingLeft: theme.spacing(4),
40
+                paddingRight: theme.spacing(4),
41
+                marginLeft: `-${theme.spacing(4)}`,
42
+                '&.large': {
43
+                    width: '616px'
44
+                },
45
+                '&.medium': {
46
+                    width: '352px'
47
+                },
48
+                '@media (max-width: 448px)': {
49
+                    width: 'calc(100% - 48px) !important'
50
+                },
51
+                '& .upper-header': {
52
+                    display: 'flex',
53
+                    justifyContent: 'space-between',
54
+                    alignItems: 'center',
55
+                    width: '100%',
56
+                    '& .search-switch-container': {
57
+                        display: 'flex',
58
+                        width: '100%',
59
+                        '& .search-container': {
60
+                            width: 175,
61
+                            marginRight: theme.spacing(3)
62
+                        },
63
+                        '& .search-container-full-width': {
64
+                            width: '100%'
65
+                        }
66
+                    },
67
+                    '& .emotions-icons': {
68
+                        display: 'flex',
69
+                        '& svg': {
70
+                            fill: '#000'
71
+                        },
72
+                        '&>div': {
73
+                            marginRight: theme.spacing(3)
74
+                        },
75
+                        '&>div:last-child': {
76
+                            marginRight: 0
77
+                        }
78
+                    }
79
+                }
80
+            },
23 81
             '& .row': {
24 82
                 display: 'flex',
25 83
                 alignItems: 'center',
26
-
27
-                '& .avatar': {
28
-                    width: '32px',
29
-                    marginRight: theme.spacing(3)
30
-                },
31
-
32 84
                 '& .name-time': {
33 85
                     width: 'calc(100% - 48px)',
34 86
                     display: 'flex',
35 87
                     justifyContent: 'space-between',
36
-                    alignItems: 'center'
88
+                    alignItems: 'center',
89
+                    '&.expressions-on': {
90
+                        width: 'calc(47% - 48px)',
91
+                        marginRight: theme.spacing(4)
92
+                    }
37 93
                 },
38
-
39
-                '& .name-time_expressions-on': {
40
-                    width: 'calc(47% - 48px)'
94
+                '& .timeline-container': {
95
+                    height: '100%',
96
+                    width: `calc(53% - ${theme.spacing(4)})`,
97
+                    display: 'flex',
98
+                    alignItems: 'center',
99
+                    borderLeftWidth: 1,
100
+                    borderLeftColor: theme.palette.ui02,
101
+                    borderLeftStyle: 'solid',
102
+                    '& .timeline': {
103
+                        height: theme.spacing(2),
104
+                        display: 'flex',
105
+                        width: '100%',
106
+                        '&>div': {
107
+                            marginRight: theme.spacing(1),
108
+                            borderRadius: 5
109
+                        },
110
+                        '&>div:first-child': {
111
+                            borderRadius: '0 5px 5px 0'
112
+                        },
113
+                        '&>div:last-child': {
114
+                            marginRight: 0,
115
+                            borderRadius: '5px 0 0 5px'
116
+                        }
117
+                    }
41 118
                 },
42
-
43
-                '& .expressions': {
44
-                    width: 'calc(53% - 29px)',
119
+                '& .axis-container': {
120
+                    height: '100%',
121
+                    width: `calc(53% - ${theme.spacing(6)})`,
45 122
                     display: 'flex',
46
-                    justifyContent: 'space-between',
47
-
48
-                    '& .expression': {
49
-                        width: '30px',
50
-                        textAlign: 'center'
123
+                    alignItems: 'center',
124
+                    marginLeft: theme.spacing(3),
125
+                    '& div': {
126
+                        borderRadius: 5
127
+                    },
128
+                    '& .axis': {
129
+                        height: theme.spacing(1),
130
+                        display: 'flex',
131
+                        width: '100%',
132
+                        backgroundColor: theme.palette.ui03,
133
+                        position: 'relative',
134
+                        '& .left-bound': {
135
+                            position: 'absolute',
136
+                            bottom: 10,
137
+                            left: 0
138
+                        },
139
+                        '& .right-bound': {
140
+                            position: 'absolute',
141
+                            bottom: 10,
142
+                            right: 0
143
+                        },
144
+                        '& .handler': {
145
+                            position: 'absolute',
146
+                            backgroundColor: theme.palette.ui09,
147
+                            height: 12,
148
+                            marginTop: -4,
149
+                            display: 'flex',
150
+                            justifyContent: 'space-between',
151
+                            '& .resize': {
152
+                                height: '100%',
153
+                                width: 5,
154
+                                cursor: 'col-resize'
155
+                            }
156
+                        }
51 157
                     }
52 158
                 }
159
+            },
160
+            '& .separator': {
161
+                width: 'calc(100% + 48px)',
162
+                height: 1,
163
+                marginLeft: -24,
164
+                backgroundColor: theme.palette.ui02
53 165
             }
54
-        },
55
-        labelsContainer: {
56
-            position: 'relative'
57
-        },
58
-        separator: {
59
-            position: 'absolute',
60
-            width: 'calc(100% + 48px)',
61
-            height: 1,
62
-            left: -24,
63
-            backgroundColor: theme.palette.ui05
64
-        },
65
-        searchSwitchContainer: {
66
-            display: 'flex',
67
-            justifyContent: 'space-between',
68
-            alignItems: 'center',
69
-            width: '100%'
70
-        },
71
-        searchSwitchContainerExpressionsOn: {
72
-            width: '58.5%',
73
-            [theme.breakpoints.down(RESIZE_SEARCH_SWITCH_CONTAINER_BREAKPOINT)]: {
74
-                width: '100%'
75
-            }
76
-        },
77
-        searchContainer: {
78
-            width: '50%'
79
-        },
80
-        searchContainerFullWidth: {
81
-            width: '100%'
82 166
         }
83 167
     };
84 168
 });
85 169
 
170
+const EMOTIONS_LEGEND = [
171
+    {
172
+        translationKey: 'speakerStats.neutral',
173
+        icon: IconEmotionsNeutral
174
+    },
175
+    {
176
+        translationKey: 'speakerStats.happy',
177
+        icon: IconEmotionsHappy
178
+    },
179
+    {
180
+        translationKey: 'speakerStats.surprised',
181
+        icon: IconEmotionsSurprised
182
+    },
183
+    {
184
+        translationKey: 'speakerStats.sad',
185
+        icon: IconEmotionsSad
186
+    },
187
+    {
188
+        translationKey: 'speakerStats.fearful',
189
+        icon: IconEmotionsFearful
190
+    },
191
+    {
192
+        translationKey: 'speakerStats.angry',
193
+        icon: IconEmotionsAngry
194
+    },
195
+    {
196
+        translationKey: 'speakerStats.disgusted',
197
+        icon: IconEmotionsDisgusted
198
+    }
199
+];
200
+
86 201
 const SpeakerStats = () => {
87 202
     const { faceLandmarks } = useSelector((state: IReduxState) => state['features/base/config']);
88 203
     const { showFaceExpressions } = useSelector((state: IReduxState) => state['features/speaker-stats']);
@@ -91,6 +206,7 @@ const SpeakerStats = () => {
91 206
     const displayLabels = clientWidth > MOBILE_BREAKPOINT;
92 207
     const dispatch = useDispatch();
93 208
     const { classes } = useStyles();
209
+    const { t } = useTranslation();
94 210
 
95 211
     const onToggleFaceExpressions = useCallback(() =>
96 212
         dispatch(toggleFaceExpressions())
@@ -104,9 +220,9 @@ const SpeakerStats = () => {
104 220
     useEffect(() => {
105 221
         showFaceExpressions && !displaySwitch && dispatch(toggleFaceExpressions());
106 222
     }, [ clientWidth ]);
107
-    useEffect(() => () => {
108
-        dispatch(resetSearchCriteria());
109
-    }, []);
223
+
224
+    // @ts-ignore
225
+    useEffect(() => () => dispatch(resetSearchCriteria()), []);
110 226
 
111 227
     return (
112 228
         <Dialog
@@ -115,33 +231,49 @@ const SpeakerStats = () => {
115 231
             size = { showFaceExpressions ? 'large' : 'medium' }
116 232
             titleKey = 'speakerStats.speakerStats'>
117 233
             <div className = { classes.speakerStats }>
118
-                <div
119
-                    className = {
120
-                        `${classes.searchSwitchContainer}
121
-                        ${showFaceExpressions ? classes.searchSwitchContainerExpressionsOn : ''}`
122
-                    }>
123
-                    <div
124
-                        className = {
125
-                            displaySwitch
126
-                                ? classes.searchContainer
127
-                                : classes.searchContainerFullWidth }>
128
-                        <SpeakerStatsSearch
129
-                            onSearch = { onSearch } />
130
-                    </div>
234
+                <div className = { `header ${showFaceExpressions ? 'large' : 'medium'}` }>
235
+                    <div className = 'upper-header'>
236
+                        <div
237
+                            className = {
238
+                                `search-switch-container
239
+                        ${showFaceExpressions ? 'expressions-on' : ''}`
240
+                            }>
241
+                            <div
242
+                                className = {
243
+                                    displaySwitch
244
+                                        ? 'search-container'
245
+                                        : 'search-container-full-width' }>
246
+                                <SpeakerStatsSearch
247
+                                    onSearch = { onSearch } />
248
+                            </div>
131 249
 
132
-                    { displaySwitch
250
+                            { displaySwitch
133 251
                     && <FaceExpressionsSwitch
134 252
                         onChange = { onToggleFaceExpressions }
135 253
                         showFaceExpressions = { showFaceExpressions } />
136
-                    }
137
-                </div>
138
-                { displayLabels && (
139
-                    <div className = { classes.labelsContainer }>
254
+
255
+                            }
256
+                        </div>
257
+                        { showFaceExpressions && <div className = 'emotions-icons'>
258
+                            {
259
+                                EMOTIONS_LEGEND.map(emotion => (
260
+                                    <Tooltip
261
+                                        content = { t(emotion.translationKey) }
262
+                                        key = { emotion.translationKey }
263
+                                        position = { 'top' }>
264
+                                        <Icon
265
+                                            size = { 20 }
266
+                                            src = { emotion.icon } />
267
+                                    </Tooltip>
268
+                                ))
269
+                            }
270
+                        </div>}
271
+                    </div>
272
+                    { displayLabels && (
140 273
                         <SpeakerStatsLabels
141 274
                             showFaceExpressions = { showFaceExpressions ?? false } />
142
-                        <div className = { classes.separator } />
143
-                    </div>
144
-                )}
275
+                    )}
276
+                </div>
145 277
                 <SpeakerStatsList />
146 278
             </div>
147 279
         </Dialog>

react/features/speaker-stats/components/web/SpeakerStatsButton.js → react/features/speaker-stats/components/web/SpeakerStatsButton.tsx Parādīt failu

@@ -1,12 +1,12 @@
1
-// @flow
2
-
3
-import { createToolbarEvent, sendAnalytics } from '../../../analytics';
4
-import { openDialog } from '../../../base/dialog';
5
-import { translate } from '../../../base/i18n';
6
-import { connect } from '../../../base/redux';
1
+import { createToolbarEvent } from '../../../analytics/AnalyticsEvents';
2
+import { sendAnalytics } from '../../../analytics/functions';
3
+import { openDialog } from '../../../base/dialog/actions';
4
+import { translate } from '../../../base/i18n/functions';
5
+import { connect } from '../../../base/redux/functions';
7 6
 import AbstractSpeakerStatsButton from '../AbstractSpeakerStatsButton';
8 7
 
9
-import { SpeakerStats } from './';
8
+import SpeakerStats from './SpeakerStats';
9
+
10 10
 
11 11
 /**
12 12
  * Implementation of a button for opening speaker stats dialog.
@@ -20,6 +20,7 @@ class SpeakerStatsButton extends AbstractSpeakerStatsButton {
20 20
      * @returns {void}
21 21
      */
22 22
     _handleClick() {
23
+        // @ts-ignore
23 24
         const { dispatch } = this.props;
24 25
 
25 26
         sendAnalytics(createToolbarEvent('speaker.stats'));
@@ -27,4 +28,5 @@ class SpeakerStatsButton extends AbstractSpeakerStatsButton {
27 28
     }
28 29
 }
29 30
 
31
+// @ts-ignore
30 32
 export default translate(connect()(SpeakerStatsButton));

+ 0
- 136
react/features/speaker-stats/components/web/SpeakerStatsItem.js Parādīt failu

@@ -1,136 +0,0 @@
1
-/* @flow */
2
-
3
-import React from 'react';
4
-
5
-import { Avatar, StatelessAvatar } from '../../../base/avatar';
6
-import { getInitials } from '../../../base/avatar/functions';
7
-import BaseTheme from '../../../base/ui/components/BaseTheme';
8
-import { FACE_EXPRESSIONS } from '../../../face-landmarks/constants';
9
-
10
-import TimeElapsed from './TimeElapsed';
11
-
12
-/**
13
- * The type of the React {@code Component} props of {@link SpeakerStatsItem}.
14
- */
15
-type Props = {
16
-
17
-    /**
18
-     * The name of the participant.
19
-     */
20
-    displayName: string,
21
-
22
-    /**
23
-     * The object that has as keys the face expressions of the
24
-     * participant and as values a number that represents the count .
25
-     */
26
-    faceExpressions: Object,
27
-
28
-    /**
29
-     * True if the face expressions detection is not disabled.
30
-     */
31
-    showFaceExpressions: boolean,
32
-
33
-    /**
34
-     * The total milliseconds the participant has been dominant speaker.
35
-     */
36
-    dominantSpeakerTime: number,
37
-
38
-    /**
39
-     * The id of the user.
40
-     */
41
-    participantId: string,
42
-
43
-    /**
44
-     * True if the participant is no longer in the meeting.
45
-     */
46
-    hasLeft: boolean,
47
-
48
-    /**
49
-     * True if the participant is not shown in speaker stats.
50
-     */
51
-    hidden: boolean,
52
-
53
-    /**
54
-     * True if the participant is currently the dominant speaker.
55
-     */
56
-    isDominantSpeaker: boolean,
57
-
58
-    /**
59
-     * Styles for the item.
60
-     */
61
-    styles: Object,
62
-
63
-    /**
64
-     * Invoked to obtain translated strings.
65
-     */
66
-    t: Function
67
-}
68
-
69
-const SpeakerStatsItem = (props: Props) => {
70
-    const hasLeftClass = props.hasLeft ? props.styles.hasLeft : '';
71
-    const rowDisplayClass = `row ${hasLeftClass} ${props.styles.item}`;
72
-    const expressionClass = 'expression';
73
-    const nameTimeClass = `name-time${
74
-        props.showFaceExpressions ? ' name-time_expressions-on' : ''
75
-    }`;
76
-    const timeClass = `${props.styles.time} ${props.isDominantSpeaker ? props.styles.dominant : ''}`;
77
-
78
-
79
-    const FaceExpressions = () => FACE_EXPRESSIONS.map(
80
-            expression => (
81
-                <div
82
-                    aria-label = { props.t(`speakerStats.${expression}`) }
83
-                    className = {
84
-                        `${expressionClass} ${
85
-                            props.faceExpressions[expression] === 0 ? props.styles.hasLeft : ''
86
-                        }`
87
-                    }
88
-                    key = { expression }>
89
-                    { props.faceExpressions[expression] }
90
-                </div>
91
-            )
92
-    );
93
-
94
-    return (
95
-        <div
96
-            className = { rowDisplayClass }
97
-            key = { props.participantId } >
98
-            <div className = { `avatar ${props.styles.avatar}` }>
99
-                {
100
-                    props.hasLeft ? (
101
-                        <StatelessAvatar
102
-                            className = 'userAvatar'
103
-                            color = { BaseTheme.palette.ui04 }
104
-                            id = 'avatar'
105
-                            initials = { getInitials(props.displayName) } />
106
-                    ) : (
107
-                        <Avatar
108
-                            className = 'userAvatar'
109
-                            participantId = { props.participantId } />
110
-                    )
111
-                }
112
-            </div>
113
-            <div className = { nameTimeClass }>
114
-                <div
115
-                    aria-label = { props.t('speakerStats.speakerStats') }
116
-                    className = { props.styles.displayName }>
117
-                    { props.displayName }
118
-                </div>
119
-                <div
120
-                    aria-label = { props.t('speakerStats.speakerTime') }
121
-                    className = { timeClass }>
122
-                    <TimeElapsed
123
-                        time = { props.dominantSpeakerTime } />
124
-                </div>
125
-            </div>
126
-            { props.showFaceExpressions
127
-            && (
128
-                <div className = { `expressions ${props.styles.expressions}` }>
129
-                    <FaceExpressions />
130
-                </div>
131
-            )}
132
-        </div>
133
-    );
134
-};
135
-
136
-export default SpeakerStatsItem;

+ 115
- 0
react/features/speaker-stats/components/web/SpeakerStatsItem.tsx Parādīt failu

@@ -0,0 +1,115 @@
1
+// eslint-disable-next-line lines-around-comment
2
+import React from 'react';
3
+
4
+// @ts-ignore
5
+import Avatar from '../../../base/avatar/components/Avatar';
6
+import StatelessAvatar from '../../../base/avatar/components/web/StatelessAvatar';
7
+import { getInitials } from '../../../base/avatar/functions';
8
+import BaseTheme from '../../../base/ui/components/BaseTheme.web';
9
+import { FaceLandmarks } from '../../../face-landmarks/types';
10
+
11
+import TimeElapsed from './TimeElapsed';
12
+import Timeline from './Timeline';
13
+
14
+/**
15
+ * The type of the React {@code Component} props of {@link SpeakerStatsItem}.
16
+ */
17
+type Props = {
18
+
19
+    /**
20
+     * The name of the participant.
21
+     */
22
+    displayName: string;
23
+
24
+    /**
25
+     * The total milliseconds the participant has been dominant speaker.
26
+     */
27
+    dominantSpeakerTime: number;
28
+
29
+    /**
30
+     * The object that has as keys the face expressions of the
31
+     * participant and as values a number that represents the count .
32
+     */
33
+    faceLandmarks?: FaceLandmarks[];
34
+
35
+    /**
36
+     * True if the participant is no longer in the meeting.
37
+     */
38
+    hasLeft: boolean;
39
+
40
+    /**
41
+     * True if the participant is not shown in speaker stats.
42
+     */
43
+    hidden: boolean;
44
+
45
+    /**
46
+     * True if the participant is currently the dominant speaker.
47
+     */
48
+    isDominantSpeaker: boolean;
49
+
50
+    /**
51
+     * The id of the user.
52
+     */
53
+    participantId: string;
54
+
55
+    /**
56
+     * True if the face expressions detection is not disabled.
57
+     */
58
+    showFaceExpressions: boolean;
59
+
60
+    /**
61
+     * Invoked to obtain translated strings.
62
+     */
63
+    t: Function;
64
+};
65
+
66
+const SpeakerStatsItem = (props: Props) => {
67
+    const rowDisplayClass = `row item ${props.hasLeft ? 'has-left' : ''}`;
68
+    const nameTimeClass = `name-time${
69
+        props.showFaceExpressions ? ' expressions-on' : ''
70
+    }`;
71
+    const timeClass = `time ${props.isDominantSpeaker ? 'dominant' : ''}`;
72
+
73
+    return (
74
+        <div key = { props.participantId }>
75
+            <div className = { rowDisplayClass } >
76
+                <div className = 'avatar' >
77
+                    {
78
+                        props.hasLeft ? (
79
+                            <StatelessAvatar
80
+                                className = 'userAvatar'
81
+                                color = { BaseTheme.palette.ui04 }
82
+                                initials = { getInitials(props.displayName) } />
83
+                        ) : (
84
+                            <Avatar
85
+
86
+                                // @ts-ignore
87
+                                className = 'userAvatar'
88
+                                participantId = { props.participantId } />
89
+                        )
90
+                    }
91
+                </div>
92
+                <div className = { nameTimeClass }>
93
+                    <div
94
+                        aria-label = { props.t('speakerStats.speakerStats') }
95
+                        className = 'display-name'>
96
+                        { props.displayName }
97
+                    </div>
98
+                    <div
99
+                        aria-label = { props.t('speakerStats.speakerTime') }
100
+                        className = { timeClass }>
101
+                        <TimeElapsed
102
+                            time = { props.dominantSpeakerTime } />
103
+                    </div>
104
+                </div>
105
+                { props.showFaceExpressions
106
+            && <Timeline faceLandmarks = { props.faceLandmarks } />
107
+                }
108
+
109
+            </div>
110
+            <div className = 'separator' />
111
+        </div>
112
+    );
113
+};
114
+
115
+export default SpeakerStatsItem;

+ 10
- 33
react/features/speaker-stats/components/web/SpeakerStatsLabels.tsx Parādīt failu

@@ -2,21 +2,18 @@ import React from 'react';
2 2
 import { useTranslation } from 'react-i18next';
3 3
 import { makeStyles } from 'tss-react/mui';
4 4
 
5
-import { withPixelLineHeight } from '../../../base/styles/functions.web';
6
-// eslint-disable-next-line lines-around-comment
7
-// @ts-ignore
8
-import { Tooltip } from '../../../base/tooltip';
9
-import { FACE_EXPRESSIONS_EMOJIS } from '../../../face-landmarks/constants';
5
+import TimelineAxis from './TimelineAxis';
10 6
 
11 7
 const useStyles = makeStyles()(theme => {
12 8
     return {
13 9
         labels: {
14 10
             padding: '22px 0 7px 0',
15
-            height: 20
16
-        },
17
-        emojis: {
18
-            paddingLeft: 27,
19
-            ...withPixelLineHeight(theme.typography.bodyShortRegularLarge)
11
+            height: 20,
12
+            '& .avatar-placeholder': {
13
+                width: '32px',
14
+                marginRight: theme.spacing(3)
15
+
16
+            }
20 17
         }
21 18
     };
22 19
 });
@@ -36,12 +33,12 @@ const SpeakerStatsLabels = (props: IProps) => {
36 33
     const { t } = useTranslation();
37 34
     const { classes } = useStyles();
38 35
     const nameTimeClass = `name-time${
39
-        props.showFaceExpressions ? ' name-time_expressions-on' : ''
36
+        props.showFaceExpressions ? ' expressions-on' : ''
40 37
     }`;
41 38
 
42 39
     return (
43 40
         <div className = { `row ${classes.labels}` }>
44
-            <div className = 'avatar' />
41
+            <div className = 'avatar-placeholder' />
45 42
 
46 43
             <div className = { nameTimeClass }>
47 44
                 <div>
@@ -51,27 +48,7 @@ const SpeakerStatsLabels = (props: IProps) => {
51 48
                     { t('speakerStats.speakerTime') }
52 49
                 </div>
53 50
             </div>
54
-            {
55
-                props.showFaceExpressions
56
-                && <div className = { `expressions ${classes.emojis}` }>
57
-                    {Object.keys(FACE_EXPRESSIONS_EMOJIS).map(
58
-                        expression => (
59
-                            <div
60
-                                className = 'expression'
61
-                                key = { expression }>
62
-                                <Tooltip
63
-                                    content = { t(`speakerStats.${expression}`) }
64
-                                    position = { 'top' } >
65
-                                    <div>
66
-                                        {FACE_EXPRESSIONS_EMOJIS[expression as keyof typeof FACE_EXPRESSIONS_EMOJIS]}
67
-                                    </div>
68
-
69
-                                </Tooltip>
70
-                            </div>
71
-                        )
72
-                    )}
73
-                </div>
74
-            }
51
+            {props.showFaceExpressions && <TimelineAxis />}
75 52
         </div>
76 53
     );
77 54
 };

+ 35
- 34
react/features/speaker-stats/components/web/SpeakerStatsList.tsx Parādīt failu

@@ -13,40 +13,40 @@ import SpeakerStatsItem from './SpeakerStatsItem';
13 13
 const useStyles = makeStyles()(theme => {
14 14
     return {
15 15
         list: {
16
-            marginTop: theme.spacing(3),
17
-            marginBottom: theme.spacing(3)
18
-        },
19
-        item: {
20
-            height: theme.spacing(7),
21
-            [theme.breakpoints.down(MOBILE_BREAKPOINT)]: {
22
-                height: theme.spacing(8)
16
+            paddingTop: 90,
17
+            '& .item': {
18
+                height: theme.spacing(7),
19
+                [theme.breakpoints.down(MOBILE_BREAKPOINT)]: {
20
+                    height: theme.spacing(8)
21
+                },
22
+                '& .has-left': {
23
+                    color: theme.palette.text03
24
+                },
25
+                '& .avatar': {
26
+                    width: '32px',
27
+                    marginRight: theme.spacing(3),
28
+                    height: theme.spacing(5)
29
+                },
30
+                '& .time': {
31
+                    padding: '2px 4px',
32
+                    borderRadius: '4px',
33
+                    ...withPixelLineHeight(theme.typography.labelBold),
34
+                    [theme.breakpoints.down(MOBILE_BREAKPOINT)]: {
35
+                        ...withPixelLineHeight(theme.typography.bodyShortRegularLarge)
36
+                    },
37
+                    backgroundColor: theme.palette.ui02
38
+                },
39
+                '& .display-name': {
40
+                    ...withPixelLineHeight(theme.typography.bodyShortRegular),
41
+                    [theme.breakpoints.down(MOBILE_BREAKPOINT)]: {
42
+                        ...withPixelLineHeight(theme.typography.bodyShortRegularLarge)
43
+                    }
44
+                },
45
+                '& .dominant': {
46
+                    backgroundColor: theme.palette.success02
47
+                }
23 48
             }
24
-        },
25
-        avatar: {
26
-            height: theme.spacing(5)
27
-        },
28
-        expressions: {
29
-            paddingLeft: 29
30
-        },
31
-        hasLeft: {
32
-            color: theme.palette.text03
33
-        },
34
-        displayName: {
35
-            ...withPixelLineHeight(theme.typography.bodyShortRegular),
36
-            [theme.breakpoints.down(MOBILE_BREAKPOINT)]: {
37
-                ...withPixelLineHeight(theme.typography.bodyShortRegularLarge)
38
-            }
39
-        },
40
-        time: {
41
-            padding: '2px 4px',
42
-            borderRadius: '4px',
43
-            ...withPixelLineHeight(theme.typography.labelBold),
44
-            [theme.breakpoints.down(MOBILE_BREAKPOINT)]: {
45
-                ...withPixelLineHeight(theme.typography.bodyShortRegularLarge)
46
-            }
47
-        },
48
-        dominant: {
49
-            backgroundColor: theme.palette.success02
49
+
50 50
         }
51 51
     };
52 52
 });
@@ -58,10 +58,11 @@ const useStyles = makeStyles()(theme => {
58 58
  */
59 59
 const SpeakerStatsList = () => {
60 60
     const { classes } = useStyles();
61
-    const items = abstractSpeakerStatsList(SpeakerStatsItem, classes);
61
+    const items = abstractSpeakerStatsList(SpeakerStatsItem);
62 62
 
63 63
     return (
64 64
         <div className = { classes.list }>
65
+            <div className = 'separator' />
65 66
             {items}
66 67
         </div>
67 68
     );

+ 0
- 50
react/features/speaker-stats/components/web/TimeElapsed.js Parādīt failu

@@ -1,50 +0,0 @@
1
-/* @flow */
2
-
3
-import React, { Component } from 'react';
4
-
5
-import { translate } from '../../../base/i18n';
6
-import { createLocalizedTime } from '../timeFunctions';
7
-
8
-/**
9
- * The type of the React {@code Component} props of {@link TimeElapsed}.
10
- */
11
-type Props = {
12
-
13
-    /**
14
-     * The function to translate human-readable text.
15
-     */
16
-    t: Function,
17
-
18
-    /**
19
-     * The milliseconds to be converted into a human-readable format.
20
-     */
21
-    time: number
22
-};
23
-
24
-/**
25
- * React component for displaying total time elapsed. Converts a total count of
26
- * milliseconds into a more humanized form: "# hours, # minutes, # seconds".
27
- * With a time of 0, "0s" will be displayed.
28
- *
29
- * @augments Component
30
- */
31
-class TimeElapsed extends Component<Props> {
32
-    /**
33
-     * Implements React's {@link Component#render()}.
34
-     *
35
-     * @inheritdoc
36
-     * @returns {ReactElement}
37
-     */
38
-    render() {
39
-        const { time, t } = this.props;
40
-        const timeElapsed = createLocalizedTime(time, t);
41
-
42
-        return (
43
-            <div>
44
-                { timeElapsed }
45
-            </div>
46
-        );
47
-    }
48
-}
49
-
50
-export default translate(TimeElapsed);

+ 36
- 0
react/features/speaker-stats/components/web/TimeElapsed.tsx Parādīt failu

@@ -0,0 +1,36 @@
1
+import React from 'react';
2
+import { useTranslation } from 'react-i18next';
3
+
4
+import { createLocalizedTime } from '../timeFunctions';
5
+
6
+/**
7
+ * The type of the React {@code Component} props of {@link TimeElapsed}.
8
+ */
9
+type Props = {
10
+
11
+    /**
12
+     * The milliseconds to be converted into a human-readable format.
13
+     */
14
+    time: number;
15
+};
16
+
17
+/**
18
+ * React component for displaying total time elapsed. Converts a total count of
19
+ * milliseconds into a more humanized form: "# hours, # minutes, # seconds".
20
+ * With a time of 0, "0s" will be displayed.
21
+ *
22
+ * @augments Component
23
+ */
24
+
25
+const TimeElapsed = ({ time }: Props) => {
26
+    const { t } = useTranslation();
27
+    const timeElapsed = createLocalizedTime(time, t);
28
+
29
+    return (
30
+        <span>
31
+            { timeElapsed }
32
+        </span>
33
+    );
34
+};
35
+
36
+export default TimeElapsed;

+ 207
- 0
react/features/speaker-stats/components/web/Timeline.tsx Parādīt failu

@@ -0,0 +1,207 @@
1
+import React, { MouseEvent, useCallback, useEffect, useMemo, useRef } from 'react';
2
+import { useDispatch, useSelector } from 'react-redux';
3
+
4
+import { IReduxState } from '../../../app/types';
5
+import { getConferenceTimestamp } from '../../../base/conference/functions';
6
+import { FaceLandmarks } from '../../../face-landmarks/types';
7
+import { addToOffset, setTimelinePanning } from '../../actions.any';
8
+import { SCROLL_RATE, TIMELINE_COLORS } from '../../constants';
9
+import { getFaceLandmarksEnd, getFaceLandmarksStart, getTimelineBoundaries } from '../../functions';
10
+
11
+type Props = {
12
+    faceLandmarks?: FaceLandmarks[];
13
+};
14
+
15
+const Timeline = ({ faceLandmarks }: Props) => {
16
+    const startTimestamp = useSelector((state: IReduxState) => getConferenceTimestamp(state)) ?? 0;
17
+    const { left, right } = useSelector((state: IReduxState) => getTimelineBoundaries(state));
18
+    const { timelinePanning } = useSelector((state: IReduxState) => state['features/speaker-stats']);
19
+    const dispatch = useDispatch();
20
+    const containerRef = useRef<HTMLDivElement>(null);
21
+    const intervalDuration = useMemo(() => right - left, [ left, right ]);
22
+
23
+    const getSegments = useCallback(() => {
24
+        const segments = faceLandmarks?.filter(landmarks => {
25
+            const timeStart = getFaceLandmarksStart(landmarks, startTimestamp);
26
+            const timeEnd = getFaceLandmarksEnd(landmarks, startTimestamp);
27
+
28
+            if (timeEnd > left && timeStart < right) {
29
+
30
+                return true;
31
+            }
32
+
33
+            return false;
34
+        }) ?? [];
35
+
36
+        let leftCut;
37
+        let rightCut;
38
+
39
+        if (segments.length) {
40
+            const start = getFaceLandmarksStart(segments[0], startTimestamp);
41
+            const end = getFaceLandmarksEnd(segments[segments.length - 1], startTimestamp);
42
+
43
+            if (start <= left) {
44
+                leftCut = segments[0];
45
+            }
46
+            if (end >= right) {
47
+                rightCut = segments[segments.length - 1];
48
+            }
49
+        }
50
+
51
+        if (leftCut) {
52
+            segments.shift();
53
+        }
54
+        if (rightCut) {
55
+            segments.pop();
56
+        }
57
+
58
+        return {
59
+            segments,
60
+            leftCut,
61
+            rightCut
62
+        };
63
+    }, [ faceLandmarks, left, right, startTimestamp ]);
64
+
65
+    const { segments, leftCut, rightCut } = getSegments();
66
+
67
+    const getStyle = useCallback((duration: number, faceExpression: string) => {
68
+        return {
69
+            width: `${100 / (intervalDuration / duration)}%`,
70
+            backgroundColor: TIMELINE_COLORS[faceExpression] ?? TIMELINE_COLORS['no-detection']
71
+        };
72
+    }, [ intervalDuration ]);
73
+
74
+
75
+    const getStartStyle = useCallback(() => {
76
+        let startDuration = 0;
77
+        let color = TIMELINE_COLORS['no-detection'];
78
+
79
+        if (leftCut) {
80
+            const { faceExpression } = leftCut;
81
+
82
+            startDuration = getFaceLandmarksEnd(leftCut, startTimestamp) - left;
83
+            color = TIMELINE_COLORS[faceExpression];
84
+        } else if (segments.length) {
85
+            startDuration = getFaceLandmarksStart(segments[0], startTimestamp) - left;
86
+        } else if (rightCut) {
87
+            startDuration = getFaceLandmarksStart(rightCut, startTimestamp) - left;
88
+        }
89
+
90
+        return {
91
+            width: `${100 / (intervalDuration / startDuration)}%`,
92
+            backgroundColor: color
93
+        };
94
+    }, [ leftCut, rightCut, startTimestamp, left, intervalDuration, segments ]);
95
+
96
+    const getEndStyle = useCallback(() => {
97
+        let endDuration = 0;
98
+        let color = TIMELINE_COLORS['no-detection'];
99
+
100
+        if (rightCut) {
101
+            const { faceExpression } = rightCut;
102
+
103
+            endDuration = right - getFaceLandmarksStart(rightCut, startTimestamp);
104
+            color = TIMELINE_COLORS[faceExpression];
105
+        } else if (segments.length) {
106
+            endDuration = right - getFaceLandmarksEnd(segments[segments.length - 1], startTimestamp);
107
+        } else if (leftCut) {
108
+            endDuration = right - getFaceLandmarksEnd(leftCut, startTimestamp);
109
+        }
110
+
111
+        return {
112
+            width: `${100 / (intervalDuration / endDuration)}%`,
113
+            backgroundColor: color
114
+        };
115
+    }, [ leftCut, rightCut, startTimestamp, right, intervalDuration, segments ]);
116
+
117
+    const getOneSegmentStyle = useCallback((faceExpression?: string) => {
118
+        return {
119
+            width: '100%',
120
+            backgroundColor: faceExpression ? TIMELINE_COLORS[faceExpression] : TIMELINE_COLORS['no-detection'],
121
+            borderRadius: 0
122
+        };
123
+    }, []);
124
+
125
+    const handleOnWheel = useCallback((event: WheelEvent) => {
126
+        // check if horizontal scroll
127
+        if (Math.abs(event.deltaX) >= Math.abs(event.deltaY)) {
128
+            const value = event.deltaX * SCROLL_RATE;
129
+
130
+            dispatch(addToOffset(value));
131
+            event.preventDefault();
132
+        }
133
+    }, [ dispatch, addToOffset ]);
134
+
135
+    const hideStartAndEndSegments = useCallback(() => leftCut && rightCut
136
+                    && leftCut.faceExpression === rightCut.faceExpression
137
+                    && !segments.length,
138
+    [ leftCut, rightCut, segments ]);
139
+
140
+    useEffect(() => {
141
+        containerRef.current?.addEventListener('wheel', handleOnWheel, { passive: false });
142
+
143
+        return () => containerRef.current?.removeEventListener('wheel', handleOnWheel);
144
+    }, []);
145
+
146
+    const getPointOnTimeline = useCallback((event: MouseEvent) => {
147
+        const axisRect = event.currentTarget.getBoundingClientRect();
148
+        const eventOffsetX = event.pageX - axisRect.left;
149
+
150
+        return (eventOffsetX * right) / axisRect.width;
151
+    }, [ right ]);
152
+
153
+
154
+    const handleOnMouseMove = useCallback((event: MouseEvent) => {
155
+        const { active, x } = timelinePanning;
156
+
157
+        if (active) {
158
+            const point = getPointOnTimeline(event);
159
+
160
+            dispatch(addToOffset(x - point));
161
+            dispatch(setTimelinePanning({ ...timelinePanning,
162
+                x: point }));
163
+        }
164
+    }, [ timelinePanning, dispatch, addToOffset, setTimelinePanning, getPointOnTimeline ]);
165
+
166
+    const handleOnMouseDown = useCallback((event: MouseEvent) => {
167
+        const point = getPointOnTimeline(event);
168
+
169
+        dispatch(setTimelinePanning(
170
+                {
171
+                    active: true,
172
+                    x: point
173
+                }
174
+        ));
175
+
176
+        event.preventDefault();
177
+        event.stopPropagation();
178
+    }, [ getPointOnTimeline, dispatch, setTimelinePanning ]);
179
+
180
+    return (
181
+        <div
182
+            className = 'timeline-container'
183
+            onMouseDown = { handleOnMouseDown }
184
+            onMouseMove = { handleOnMouseMove }
185
+            ref = { containerRef }>
186
+            <div
187
+                className = 'timeline'>
188
+                {!hideStartAndEndSegments() && <div
189
+                    aria-label = 'start'
190
+                    style = { getStartStyle() } />}
191
+                {hideStartAndEndSegments() && <div
192
+                    style = { getOneSegmentStyle(leftCut?.faceExpression) } />}
193
+                {segments?.map(({ duration, timestamp, faceExpression }) =>
194
+                    (<div
195
+                        aria-label = { faceExpression }
196
+                        key = { timestamp }
197
+                        style = { getStyle(duration, faceExpression) } />)) }
198
+
199
+                {!hideStartAndEndSegments() && <div
200
+                    aria-label = 'end'
201
+                    style = { getEndStyle() } />}
202
+            </div>
203
+        </div>
204
+    );
205
+};
206
+
207
+export default Timeline;

+ 187
- 0
react/features/speaker-stats/components/web/TimelineAxis.tsx Parādīt failu

@@ -0,0 +1,187 @@
1
+import React, { MouseEvent, useCallback, useEffect, useRef, useState } from 'react';
2
+import { useDispatch, useSelector } from 'react-redux';
3
+
4
+import { IReduxState } from '../../../app/types';
5
+import { addToOffset, addToOffsetLeft, addToOffsetRight, setTimelinePanning } from '../../actions.any';
6
+import { MIN_HANDLER_WIDTH } from '../../constants';
7
+import { getCurrentDuration, getTimelineBoundaries } from '../../functions';
8
+
9
+import TimeElapsed from './TimeElapsed';
10
+
11
+const TimelineAxis = () => {
12
+    const currentDuration = useSelector((state: IReduxState) => getCurrentDuration(state)) ?? 0;
13
+    const { left, right } = useSelector((state: IReduxState) => getTimelineBoundaries(state));
14
+    const { timelinePanning } = useSelector((state: IReduxState) => state['features/speaker-stats']);
15
+    const dispatch = useDispatch();
16
+    const axisRef = useRef<HTMLDivElement>(null);
17
+
18
+    const [ dragLeft, setDragLeft ] = useState(false);
19
+    const [ dragRight, setDragRight ] = useState(false);
20
+
21
+    const getPointOnAxis = useCallback((event: MouseEvent) => {
22
+        const axisRect = event.currentTarget.getBoundingClientRect();
23
+        const eventOffsetX = event.pageX - axisRect.left;
24
+
25
+        return (eventOffsetX * currentDuration) / axisRect.width;
26
+    }, [ currentDuration ]);
27
+
28
+    const startResizeHandlerLeft = useCallback((event: MouseEvent) => {
29
+        if (!timelinePanning.active && !dragRight) {
30
+            setDragLeft(true);
31
+        }
32
+        event.preventDefault();
33
+        event.stopPropagation();
34
+    }, [ dragRight, timelinePanning, setDragLeft ]);
35
+
36
+    const stopResizeLeft = () => {
37
+        setDragLeft(false);
38
+    };
39
+
40
+    const resizeHandlerLeft = useCallback((event: MouseEvent) => {
41
+        if (dragLeft) {
42
+            const point = getPointOnAxis(event);
43
+
44
+            if (point >= 0 && point < right) {
45
+                const value = point - left;
46
+
47
+                dispatch(addToOffsetLeft(value));
48
+            }
49
+        }
50
+    }, [ dragLeft, getPointOnAxis, dispatch, addToOffsetLeft ]);
51
+
52
+    const startResizeHandlerRight = useCallback((event: MouseEvent) => {
53
+        if (!timelinePanning.active && !dragRight) {
54
+            setDragRight(true);
55
+        }
56
+        event.preventDefault();
57
+        event.stopPropagation();
58
+    }, [ timelinePanning, dragRight ]);
59
+
60
+    const stopResizeRight = useCallback(() => {
61
+        setDragRight(false);
62
+    }, [ setDragRight ]);
63
+
64
+    const resizeHandlerRight = (event: MouseEvent) => {
65
+        if (dragRight) {
66
+            const point = getPointOnAxis(event);
67
+
68
+            if (point > left && point <= currentDuration) {
69
+                const value = point - right;
70
+
71
+                dispatch(addToOffsetRight(value));
72
+            }
73
+        }
74
+    };
75
+
76
+    const startMoveHandler = useCallback((event: MouseEvent) => {
77
+        if (!dragLeft && !dragRight) {
78
+            const point = getPointOnAxis(event);
79
+
80
+            dispatch(setTimelinePanning(
81
+                {
82
+                    active: true,
83
+                    x: point
84
+                }
85
+            ));
86
+        }
87
+        event.preventDefault();
88
+        event.stopPropagation();
89
+    }, [ dragLeft, dragRight, getPointOnAxis, dispatch, setTimelinePanning ]);
90
+
91
+    const stopMoveHandler = () => {
92
+        dispatch(setTimelinePanning({ ...timelinePanning,
93
+            active: false }));
94
+    };
95
+
96
+    const moveHandler = useCallback((event: MouseEvent) => {
97
+        const { active, x } = timelinePanning;
98
+
99
+        if (active) {
100
+            const point = getPointOnAxis(event);
101
+
102
+            dispatch(addToOffset(point - x));
103
+            dispatch(setTimelinePanning({ ...timelinePanning,
104
+                x: point }));
105
+        }
106
+    }, [ timelinePanning, getPointOnAxis, dispatch, addToOffset, setTimelinePanning ]);
107
+
108
+    const handleOnMouseMove = useCallback((event: MouseEvent) => {
109
+        resizeHandlerLeft(event);
110
+        resizeHandlerRight(event);
111
+        moveHandler(event);
112
+    }, [ resizeHandlerLeft, resizeHandlerRight ]);
113
+
114
+    const handleOnMouseUp = useCallback(() => {
115
+        stopResizeLeft();
116
+        stopResizeRight();
117
+        stopMoveHandler();
118
+    }, [ stopResizeLeft, stopResizeRight, stopMoveHandler ]);
119
+
120
+    const getHandlerStyle = useCallback(() => {
121
+        let marginLeft = 100 / (currentDuration / left);
122
+        let width = 100 / (currentDuration / (right - left));
123
+
124
+        if (axisRef.current) {
125
+            const axisWidth = axisRef.current.getBoundingClientRect().width;
126
+            let handlerWidth = (width / 100) * axisWidth;
127
+
128
+            if (handlerWidth < MIN_HANDLER_WIDTH) {
129
+                const newLeft = right - ((currentDuration * MIN_HANDLER_WIDTH) / axisWidth);
130
+
131
+                handlerWidth = MIN_HANDLER_WIDTH;
132
+                marginLeft = 100 / (currentDuration / newLeft);
133
+                width = 100 / (currentDuration / (right - newLeft));
134
+            }
135
+
136
+            if (marginLeft + width > 100) {
137
+                return {
138
+                    marginLeft: `calc(100% - ${handlerWidth}px)`,
139
+                    width: handlerWidth
140
+                };
141
+            }
142
+        }
143
+
144
+        return {
145
+            marginLeft: `${marginLeft > 0 ? marginLeft : 0}%`,
146
+            width: `${width}%`
147
+        };
148
+    }, [ currentDuration, left, right, axisRef ]);
149
+
150
+    useEffect(() => {
151
+        window.addEventListener('mouseup', handleOnMouseUp);
152
+
153
+        return () => window.removeEventListener('mouseup', handleOnMouseUp);
154
+    }, []);
155
+
156
+    return (
157
+        <div
158
+            className = 'axis-container'
159
+            onMouseMove = { handleOnMouseMove }
160
+            ref = { axisRef }>
161
+            <div
162
+                className = 'axis'>
163
+                <div className = 'left-bound'>
164
+                    <TimeElapsed time = { 0 } />
165
+                </div>
166
+                <div className = 'right-bound'>
167
+                    <TimeElapsed time = { currentDuration } />
168
+                </div>
169
+                <div
170
+                    className = 'handler'
171
+                    onMouseDown = { startMoveHandler }
172
+                    style = { getHandlerStyle() } >
173
+                    <div
174
+                        className = 'resize'
175
+                        id = 'left'
176
+                        onMouseDown = { startResizeHandlerLeft } />
177
+                    <div
178
+                        className = 'resize'
179
+                        id = 'right'
180
+                        onMouseDown = { startResizeHandlerRight } />
181
+                </div>
182
+            </div>
183
+        </div>
184
+    );
185
+};
186
+
187
+export default TimelineAxis;

react/features/speaker-stats/components/web/index.js → react/features/speaker-stats/components/web/index.ts Parādīt failu


+ 23
- 2
react/features/speaker-stats/constants.ts Parādīt failu

@@ -2,6 +2,27 @@ export const SPEAKER_STATS_RELOAD_INTERVAL = 1000;
2 2
 
3 3
 export const DISPLAY_SWITCH_BREAKPOINT = 600;
4 4
 
5
-export const RESIZE_SEARCH_SWITCH_CONTAINER_BREAKPOINT = 750;
6
-
7 5
 export const MOBILE_BREAKPOINT = 480;
6
+
7
+export const THRESHOLD_FIXED_AXIS = 10000;
8
+
9
+export const MINIMUM_INTERVAL = 4000;
10
+
11
+export const SCROLL_RATE = 500;
12
+
13
+export const MIN_HANDLER_WIDTH = 30;
14
+
15
+export const TIMELINE_COLORS: {
16
+    [key: string]: string;
17
+} = {
18
+    happy: '#F3AD26',
19
+    neutral: '#676767',
20
+    sad: '#539EF9',
21
+    surprised: '#BC72E1',
22
+    angry: '#F35826',
23
+    fearful: '#3AC8C8',
24
+    disgusted: '#65B16B',
25
+    'no-detection': '#FFFFFF00'
26
+};
27
+
28
+export const CLEAR_TIME_BOUNDARY_THRESHOLD = 1000;

+ 85
- 21
react/features/speaker-stats/functions.ts Parādīt failu

@@ -1,8 +1,13 @@
1 1
 import _ from 'lodash';
2 2
 
3 3
 import { IReduxState } from '../app/types';
4
+import { getConferenceTimestamp } from '../base/conference/functions';
4 5
 import { PARTICIPANT_ROLE } from '../base/participants/constants';
5 6
 import { getParticipantById } from '../base/participants/functions';
7
+import { FaceLandmarks } from '../face-landmarks/types';
8
+
9
+import { THRESHOLD_FIXED_AXIS } from './constants';
10
+import { ISpeaker, ISpeakerStats } from './reducer';
6 11
 
7 12
 /**
8 13
  * Checks if the speaker stats search is disabled.
@@ -71,12 +76,12 @@ export function getPendingReorder(state: IReduxState) {
71 76
 /**
72 77
  * Get sorted speaker stats ids based on a configuration setting.
73 78
  *
74
- * @param {IReduxState} state - The redux state.
75
- * @param {Object} stats - The current speaker stats.
76
- * @returns {Object} - Ordered speaker stats ids.
79
+ * @param {IState} state - The redux state.
80
+ * @param {IState} stats - The current speaker stats.
81
+ * @returns {string[] | undefined} - Ordered speaker stats ids.
77 82
  * @public
78 83
  */
79
-export function getSortedSpeakerStatsIds(state: IReduxState, stats: Object) {
84
+export function getSortedSpeakerStatsIds(state: IReduxState, stats: ISpeakerStats) {
80 85
     const orderConfig = getSpeakerStatsOrder(state);
81 86
 
82 87
     if (orderConfig) {
@@ -91,11 +96,11 @@ export function getSortedSpeakerStatsIds(state: IReduxState, stats: Object) {
91 96
      *
92 97
      * Compares the order of two participants in the speaker stats list.
93 98
      *
94
-     * @param {Object} currentParticipant - The first participant for comparison.
95
-     * @param {Object} nextParticipant - The second participant for comparison.
99
+     * @param {ISpeaker} currentParticipant - The first participant for comparison.
100
+     * @param {ISpeaker} nextParticipant - The second participant for comparison.
96 101
      * @returns {number} - The sort order of the two participants.
97 102
      */
98
-    function compareFn(currentParticipant: any, nextParticipant: any) {
103
+    function compareFn(currentParticipant: ISpeaker, nextParticipant: ISpeaker) {
99 104
         if (orderConfig.includes('hasLeft')) {
100 105
             if (nextParticipant.hasLeft() && !currentParticipant.hasLeft()) {
101 106
                 return -1;
@@ -104,7 +109,7 @@ export function getSortedSpeakerStatsIds(state: IReduxState, stats: Object) {
104 109
             }
105 110
         }
106 111
 
107
-        let result;
112
+        let result = 0;
108 113
 
109 114
         for (const sortCriteria of orderConfig) {
110 115
             switch (sortCriteria) {
@@ -136,13 +141,13 @@ export function getSortedSpeakerStatsIds(state: IReduxState, stats: Object) {
136 141
 /**
137 142
  * Enhance speaker stats to include data needed for ordering.
138 143
  *
139
- * @param {IReduxState} state - The redux state.
140
- * @param {Object} stats - Speaker stats.
144
+ * @param {IState} state - The redux state.
145
+ * @param {ISpeakerStats} stats - Speaker stats.
141 146
  * @param {Array<string>} orderConfig - Ordering configuration.
142
- * @returns {Object} - Enhanced speaker stats.
147
+ * @returns {ISpeakerStats} - Enhanced speaker stats.
143 148
  * @public
144 149
  */
145
-function getEnhancedStatsForOrdering(state: IReduxState, stats: any, orderConfig?: string[]) {
150
+function getEnhancedStatsForOrdering(state: IReduxState, stats: ISpeakerStats, orderConfig: Array<string>) {
146 151
     if (!orderConfig) {
147 152
         return stats;
148 153
     }
@@ -163,14 +168,14 @@ function getEnhancedStatsForOrdering(state: IReduxState, stats: any, orderConfig
163 168
 /**
164 169
  * Filter stats by search criteria.
165 170
  *
166
- * @param {IReduxState} state - The redux state.
167
- * @param {Object | undefined} stats - The unfiltered stats.
171
+ * @param {IState} state - The redux state.
172
+ * @param {ISpeakerStats | undefined} stats - The unfiltered stats.
168 173
  *
169
- * @returns {Object} - Filtered speaker stats.
174
+ * @returns {ISpeakerStats} - Filtered speaker stats.
170 175
  * @public
171 176
  */
172
-export function filterBySearchCriteria(state: IReduxState, stats?: Object) {
173
-    const filteredStats: any = _.cloneDeep(stats ?? getSpeakerStats(state));
177
+export function filterBySearchCriteria(state: IReduxState, stats?: ISpeakerStats) {
178
+    const filteredStats = _.cloneDeep(stats ?? getSpeakerStats(state));
174 179
     const criteria = getSearchCriteria(state);
175 180
 
176 181
     if (criteria !== null) {
@@ -191,14 +196,14 @@ export function filterBySearchCriteria(state: IReduxState, stats?: Object) {
191 196
 /**
192 197
  * Reset the hidden speaker stats.
193 198
  *
194
- * @param {IReduxState} state - The redux state.
195
- * @param {Object | undefined} stats - The unfiltered stats.
199
+ * @param {IState} state - The redux state.
200
+ * @param {ISpeakerStats | undefined} stats - The unfiltered stats.
196 201
  *
197 202
  * @returns {Object} - Speaker stats.
198 203
  * @public
199 204
  */
200
-export function resetHiddenStats(state: IReduxState, stats?: Object) {
201
-    const resetStats: any = _.cloneDeep(stats ?? getSpeakerStats(state));
205
+export function resetHiddenStats(state: IReduxState, stats?: ISpeakerStats) {
206
+    const resetStats = _.cloneDeep(stats ?? getSpeakerStats(state));
202 207
 
203 208
     for (const id in resetStats) {
204 209
         if (resetStats[id].hidden) {
@@ -208,3 +213,62 @@ export function resetHiddenStats(state: IReduxState, stats?: Object) {
208 213
 
209 214
     return resetStats;
210 215
 }
216
+
217
+/**
218
+ * Gets the current duration of the conference.
219
+ *
220
+ * @param {IState} state - The redux state.
221
+ * @returns {number | null} - The duration in milliseconds or null.
222
+ */
223
+export function getCurrentDuration(state: IReduxState) {
224
+    const startTimestamp = getConferenceTimestamp(state);
225
+
226
+    return startTimestamp ? Date.now() - startTimestamp : null;
227
+}
228
+
229
+/**
230
+ * Gets the boundaries of the emotion timeline.
231
+ *
232
+ * @param {IState} state - The redux state.
233
+ * @returns {Object} - The left and right boundaries.
234
+ */
235
+export function getTimelineBoundaries(state: IReduxState) {
236
+    const { timelineBoundary, offsetLeft, offsetRight } = state['features/speaker-stats'];
237
+    const currentDuration = getCurrentDuration(state) ?? 0;
238
+    const rightBoundary = timelineBoundary ? timelineBoundary : currentDuration;
239
+    let leftOffset = 0;
240
+
241
+    if (rightBoundary > THRESHOLD_FIXED_AXIS) {
242
+        leftOffset = rightBoundary - THRESHOLD_FIXED_AXIS;
243
+    }
244
+
245
+    const left = offsetLeft + leftOffset;
246
+    const right = rightBoundary + offsetRight;
247
+
248
+    return {
249
+        left,
250
+        right
251
+    };
252
+}
253
+
254
+/**
255
+ * Returns the conference start time of the face landmarks.
256
+ *
257
+ * @param {FaceLandmarks} faceLandmarks - The face landmarks.
258
+ * @param {number} startTimestamp - The start timestamp of the conference.
259
+ * @returns {number}
260
+ */
261
+export function getFaceLandmarksStart(faceLandmarks: FaceLandmarks, startTimestamp: number) {
262
+    return faceLandmarks.timestamp - startTimestamp;
263
+}
264
+
265
+/**
266
+ * Returns the conference end time of the face landmarks.
267
+ *
268
+ * @param {FaceLandmarks} faceLandmarks - The face landmarks.
269
+ * @param {number} startTimestamp - The start timestamp of the conference.
270
+ * @returns {number}
271
+ */
272
+export function getFaceLandmarksEnd(faceLandmarks: FaceLandmarks, startTimestamp: number) {
273
+    return getFaceLandmarksStart(faceLandmarks, startTimestamp) + faceLandmarks.duration;
274
+}

react/features/speaker-stats/index.js → react/features/speaker-stats/index.ts Parādīt failu


+ 38
- 7
react/features/speaker-stats/middleware.ts Parādīt failu

@@ -1,3 +1,6 @@
1
+import { AnyAction } from 'redux';
2
+
3
+import { IStore } from '../app/types';
1 4
 import {
2 5
     PARTICIPANT_JOINED,
3 6
     PARTICIPANT_KICKED,
@@ -7,16 +10,29 @@ import {
7 10
 import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
8 11
 
9 12
 import {
13
+    ADD_TO_OFFSET,
10 14
     INIT_SEARCH,
11 15
     INIT_UPDATE_STATS,
12 16
     RESET_SEARCH_CRITERIA
13 17
 } from './actionTypes';
14
-import { initReorderStats, updateSortedSpeakerStatsIds, updateStats } from './actions';
15
-import { filterBySearchCriteria, getPendingReorder, getSortedSpeakerStatsIds, resetHiddenStats } from './functions';
16
-
17
-MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
18
-    const result = next(action);
18
+import {
19
+    clearTimelineBoundary,
20
+    initReorderStats,
21
+    setTimelineBoundary,
22
+    updateSortedSpeakerStatsIds,
23
+    updateStats
24
+} from './actions.any';
25
+import { CLEAR_TIME_BOUNDARY_THRESHOLD } from './constants';
26
+import {
27
+    filterBySearchCriteria,
28
+    getCurrentDuration,
29
+    getPendingReorder,
30
+    getSortedSpeakerStatsIds,
31
+    getTimelineBoundaries,
32
+    resetHiddenStats
33
+} from './functions';
19 34
 
35
+MiddlewareRegistry.register(({ dispatch, getState }: IStore) => (next: Function) => (action: AnyAction) => {
20 36
     switch (action.type) {
21 37
     case INIT_SEARCH: {
22 38
         const state = getState();
@@ -34,7 +50,7 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
34 50
             const pendingReorder = getPendingReorder(state);
35 51
 
36 52
             if (pendingReorder) {
37
-                dispatch(updateSortedSpeakerStatsIds(getSortedSpeakerStatsIds(state, stats)));
53
+                dispatch(updateSortedSpeakerStatsIds(getSortedSpeakerStatsIds(state, stats) ?? []));
38 54
             }
39 55
 
40 56
             dispatch(updateStats(stats));
@@ -57,7 +73,22 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
57 73
 
58 74
         break;
59 75
     }
76
+
77
+    case ADD_TO_OFFSET: {
78
+        const state = getState();
79
+        const { timelineBoundary } = state['features/speaker-stats'];
80
+        const { right } = getTimelineBoundaries(state);
81
+        const currentDuration = getCurrentDuration(state) ?? 0;
82
+
83
+        if (Math.abs((right + action.value) - currentDuration) < CLEAR_TIME_BOUNDARY_THRESHOLD) {
84
+            dispatch(clearTimelineBoundary());
85
+        } else if (!timelineBoundary) {
86
+            dispatch(setTimelineBoundary(currentDuration ?? 0));
87
+        }
88
+
89
+        break;
90
+    }
60 91
     }
61 92
 
62
-    return result;
93
+    return next(action);
63 94
 });

+ 75
- 2
react/features/speaker-stats/reducer.ts Parādīt failu

@@ -1,11 +1,17 @@
1 1
 import _ from 'lodash';
2 2
 
3 3
 import ReducerRegistry from '../base/redux/ReducerRegistry';
4
+import { FaceLandmarks } from '../face-landmarks/types';
4 5
 
5 6
 import {
7
+    ADD_TO_OFFSET,
8
+    ADD_TO_OFFSET_LEFT,
9
+    ADD_TO_OFFSET_RIGHT,
6 10
     INIT_REORDER_STATS,
7 11
     INIT_SEARCH,
8 12
     RESET_SEARCH_CRITERIA,
13
+    SET_PANNING,
14
+    SET_TIMELINE_BOUNDARY,
9 15
     TOGGLE_FACE_EXPRESSIONS,
10 16
     UPDATE_SORTED_SPEAKER_STATS_IDS,
11 17
     UPDATE_STATS
@@ -22,16 +28,52 @@ const INITIAL_STATE = {
22 28
     pendingReorder: true,
23 29
     criteria: null,
24 30
     showFaceExpressions: false,
25
-    sortedSpeakerStatsIds: []
31
+    sortedSpeakerStatsIds: [],
32
+    timelineBoundary: null,
33
+    offsetLeft: 0,
34
+    offsetRight: 0,
35
+    timelinePanning: {
36
+        active: false,
37
+        x: 0
38
+    }
26 39
 };
27 40
 
41
+export interface ISpeaker {
42
+    addFaceLandmarks: (faceLandmarks: FaceLandmarks) => void;
43
+    displayName?: string;
44
+    getDisplayName: () => string;
45
+    getFaceLandmarks: () => FaceLandmarks[];
46
+    getTotalDominantSpeakerTime: () => number;
47
+    getUserId: () => string;
48
+    hasLeft: () => boolean;
49
+    hidden?: boolean;
50
+    isDominantSpeaker: () => boolean;
51
+    isLocalStats: () => boolean;
52
+    isModerator?: boolean;
53
+    markAsHasLeft: () => boolean;
54
+    setDisplayName: (newName: string) => void;
55
+    setDominantSpeaker: (isNowDominantSpeaker: boolean, silence: boolean) => void;
56
+    setFaceLandmarks: (faceLandmarks: FaceLandmarks[]) => void;
57
+}
58
+
59
+export interface ISpeakerStats {
60
+    [key: string]: ISpeaker;
61
+}
62
+
28 63
 export interface ISpeakerStatsState {
29 64
     criteria: string | null;
30 65
     isOpen: boolean;
66
+    offsetLeft: number;
67
+    offsetRight: number;
31 68
     pendingReorder: boolean;
32 69
     showFaceExpressions: boolean;
33 70
     sortedSpeakerStatsIds: Array<string>;
34
-    stats: Object;
71
+    stats: ISpeakerStats;
72
+    timelineBoundary: number | null;
73
+    timelinePanning: {
74
+        active: boolean;
75
+        x: number;
76
+    };
35 77
 }
36 78
 
37 79
 ReducerRegistry.register<ISpeakerStatsState>('features/speaker-stats',
@@ -53,6 +95,37 @@ ReducerRegistry.register<ISpeakerStatsState>('features/speaker-stats',
53 95
             showFaceExpressions: !state.showFaceExpressions
54 96
         };
55 97
     }
98
+    case ADD_TO_OFFSET: {
99
+        return {
100
+            ...state,
101
+            offsetLeft: state.offsetLeft + action.value,
102
+            offsetRight: state.offsetRight + action.value
103
+        };
104
+    }
105
+    case ADD_TO_OFFSET_RIGHT: {
106
+        return {
107
+            ...state,
108
+            offsetRight: state.offsetRight + action.value
109
+        };
110
+    }
111
+    case ADD_TO_OFFSET_LEFT: {
112
+        return {
113
+            ...state,
114
+            offsetLeft: state.offsetLeft + action.value
115
+        };
116
+    }
117
+    case SET_TIMELINE_BOUNDARY: {
118
+        return {
119
+            ...state,
120
+            timelineBoundary: action.boundary
121
+        };
122
+    }
123
+    case SET_PANNING: {
124
+        return {
125
+            ...state,
126
+            timelinePanning: action.panning
127
+        };
128
+    }
56 129
     }
57 130
 
58 131
     return state;

+ 14
- 27
resources/prosody-plugins/mod_speakerstats_component.lua Parādīt failu

@@ -100,10 +100,10 @@ function on_message(event)
100 100
         room.speakerStats['dominantSpeakerId'] = occupant.jid;
101 101
     end
102 102
 
103
-    local faceExpression = event.stanza:get_child('faceExpression', 'http://jitsi.org/jitmeet');
103
+    local newFaceLandmarks = event.stanza:get_child('faceLandmarks', 'http://jitsi.org/jitmeet');
104 104
 
105
-    if faceExpression then
106
-        local roomAddress = faceExpression.attr.room;
105
+    if newFaceLandmarks then
106
+        local roomAddress = newFaceLandmarks.attr.room;
107 107
         local room = get_room_from_jid(room_jid_match_rewrite(roomAddress));
108 108
 
109 109
         if not room then
@@ -121,9 +121,13 @@ function on_message(event)
121 121
             log("warn", "No occupant %s found for %s", from, roomAddress);
122 122
             return false;
123 123
         end
124
-        local faceExpressions = room.speakerStats[occupant.jid].faceExpressions;
125
-        faceExpressions[faceExpression.attr.expression] =
126
-            faceExpressions[faceExpression.attr.expression] + tonumber(faceExpression.attr.duration);
124
+        local faceLandmarks = room.speakerStats[occupant.jid].faceLandmarks;
125
+        table.insert(faceLandmarks,
126
+            {
127
+                faceExpression = newFaceLandmarks.attr.faceExpression, 
128
+                timestamp = tonumber(newFaceLandmarks.attr.timestamp),
129
+                duration = tonumber(newFaceLandmarks.attr.duration),
130
+            })
127 131
     end
128 132
 
129 133
     return true
@@ -142,15 +146,7 @@ function new_SpeakerStats(nick, context_user)
142 146
         nick = nick;
143 147
         context_user = context_user;
144 148
         displayName = nil;
145
-        faceExpressions = {
146
-            happy = 0,
147
-            neutral = 0,
148
-            surprised = 0,
149
-            angry = 0,
150
-            fearful = 0,
151
-            disgusted = 0,
152
-            sad = 0
153
-        };
149
+        faceLandmarks = {};
154 150
     }, SpeakerStats);
155 151
 end
156 152
 
@@ -243,9 +239,9 @@ function occupant_joined(event)
243 239
                 -- and skip focus if sneaked into the table
244 240
                 if values and type(values) == 'table' and values.nick ~= nil and values.nick ~= 'focus' then
245 241
                     local totalDominantSpeakerTime = values.totalDominantSpeakerTime;
246
-                    local faceExpressions = values.faceExpressions;
242
+                    local faceLandmarks = values.faceLandmarks;
247 243
                     if totalDominantSpeakerTime > 0 or room:get_occupant_jid(jid) == nil or values:isDominantSpeaker()
248
-                        or get_participant_expressions_count(faceExpressions) > 0 then
244
+                        or next(faceLandmarks) ~= nil then
249 245
                         -- before sending we need to calculate current dominant speaker state
250 246
                         if values:isDominantSpeaker() and not values:isSilent() then
251 247
                             local timeElapsed = math.floor(socket.gettime()*1000 - values._dominantSpeakerStart);
@@ -255,7 +251,7 @@ function occupant_joined(event)
255 251
                         users_json[values.nick] =  {
256 252
                             displayName = values.displayName,
257 253
                             totalDominantSpeakerTime = totalDominantSpeakerTime,
258
-                            faceExpressions = faceExpressions
254
+                            faceLandmarks = faceLandmarks
259 255
                         };
260 256
                     end
261 257
                 end
@@ -391,12 +387,3 @@ process_host_module(breakout_room_component_host, function(host_module, host)
391 387
         end);
392 388
     end
393 389
 end);
394
-
395
-function get_participant_expressions_count(faceExpressions)
396
-    local count = 0;
397
-    for _, value in pairs(faceExpressions) do
398
-        count = count + value;
399
-    end
400
-
401
-    return count;
402
-end

Notiek ielāde…
Atcelt
Saglabāt