mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2025-12-30 03:12:29 +00:00
feat(local-video-recording) Allow users to record the meeting locally (#11338)
This commit is contained in:
21
config.js
21
config.js
@@ -295,6 +295,9 @@ var config = {
|
|||||||
// Whether to enable live streaming or not.
|
// Whether to enable live streaming or not.
|
||||||
// liveStreamingEnabled: false,
|
// liveStreamingEnabled: false,
|
||||||
|
|
||||||
|
// Whether to enable local recording or not.
|
||||||
|
// enableLocalRecording: false,
|
||||||
|
|
||||||
// Transcription (in interface_config,
|
// Transcription (in interface_config,
|
||||||
// subtitles and buttons can be configured)
|
// subtitles and buttons can be configured)
|
||||||
// transcribingEnabled: false,
|
// transcribingEnabled: false,
|
||||||
@@ -953,23 +956,6 @@ var config = {
|
|||||||
// ]
|
// ]
|
||||||
// },
|
// },
|
||||||
|
|
||||||
// Local Recording
|
|
||||||
//
|
|
||||||
|
|
||||||
// localRecording: {
|
|
||||||
// Enables local recording.
|
|
||||||
// Additionally, 'localrecording' (all lowercase) needs to be added to
|
|
||||||
// the `toolbarButtons`-array for the Local Recording button to show up
|
|
||||||
// on the toolbar.
|
|
||||||
//
|
|
||||||
// enabled: true,
|
|
||||||
//
|
|
||||||
|
|
||||||
// The recording format, can be one of 'ogg', 'flac' or 'wav'.
|
|
||||||
// format: 'flac'
|
|
||||||
//
|
|
||||||
|
|
||||||
// },
|
|
||||||
// e2ee: {
|
// e2ee: {
|
||||||
// labels,
|
// labels,
|
||||||
// externallyManagedKey: false
|
// externallyManagedKey: false
|
||||||
@@ -1305,7 +1291,6 @@ var config = {
|
|||||||
// 'liveStreaming.unavailableTitle', // shown when livestreaming service is not reachable
|
// 'liveStreaming.unavailableTitle', // shown when livestreaming service is not reachable
|
||||||
// 'lobby.joinRejectedMessage', // shown when while in a lobby, user's request to join is rejected
|
// 'lobby.joinRejectedMessage', // shown when while in a lobby, user's request to join is rejected
|
||||||
// 'lobby.notificationTitle', // shown when lobby is toggled and when join requests are allowed / denied
|
// 'lobby.notificationTitle', // shown when lobby is toggled and when join requests are allowed / denied
|
||||||
// 'localRecording.localRecording', // shown when a local recording is started
|
|
||||||
// 'notify.chatMessages', // shown when receiving chat messages while the chat window is closed
|
// 'notify.chatMessages', // shown when receiving chat messages while the chat window is closed
|
||||||
// 'notify.disconnected', // shown when a participant has left
|
// 'notify.disconnected', // shown when a participant has left
|
||||||
// 'notify.connectedOneMember', // show when a participant joined
|
// 'notify.connectedOneMember', // show when a participant joined
|
||||||
|
|||||||
@@ -23,7 +23,13 @@
|
|||||||
|
|
||||||
.recording-header-line {
|
.recording-header-line {
|
||||||
border-top: 1px solid #5e6d7a;
|
border-top: 1px solid #5e6d7a;
|
||||||
padding-top: 32px;
|
padding-top: 16px;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.local-recording-warning {
|
||||||
|
margin-top: 4px;
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.recording-switch-disabled {
|
.recording-switch-disabled {
|
||||||
|
|||||||
BIN
images/downloadLocalRecording.png
Normal file
BIN
images/downloadLocalRecording.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 490 B |
@@ -657,6 +657,8 @@
|
|||||||
"linkToSalesforceKey": "Link this meeting",
|
"linkToSalesforceKey": "Link this meeting",
|
||||||
"linkToSalesforceProgress": "Linking meeting to Salesforce...",
|
"linkToSalesforceProgress": "Linking meeting to Salesforce...",
|
||||||
"linkToSalesforceSuccess": "The meeting was linked to Salesforce",
|
"linkToSalesforceSuccess": "The meeting was linked to Salesforce",
|
||||||
|
"localRecordingStarted": "{{name}} has started a local recording.",
|
||||||
|
"localRecordingStopped": "{{name}} has stopped a local recording.",
|
||||||
"me": "Me",
|
"me": "Me",
|
||||||
"moderationInEffectCSDescription": "Please raise hand if you want to share your screen.",
|
"moderationInEffectCSDescription": "Please raise hand if you want to share your screen.",
|
||||||
"moderationInEffectCSTitle": "Screen sharing is blocked by the moderator",
|
"moderationInEffectCSTitle": "Screen sharing is blocked by the moderator",
|
||||||
@@ -887,6 +889,7 @@
|
|||||||
"limitNotificationDescriptionWeb": "Due to high demand your recording will be limited to {{limit}} min. For unlimited recordings try <a href={{url}} rel='noopener noreferrer' target='_blank'>{{app}}</a>.",
|
"limitNotificationDescriptionWeb": "Due to high demand your recording will be limited to {{limit}} min. For unlimited recordings try <a href={{url}} rel='noopener noreferrer' target='_blank'>{{app}}</a>.",
|
||||||
"linkGenerated": "We have generated a link to your recording.",
|
"linkGenerated": "We have generated a link to your recording.",
|
||||||
"live": "LIVE",
|
"live": "LIVE",
|
||||||
|
"localRecordingWarning": "Make sure you select the current tab in order to use the right video and audio. The recording is currently limited to 1GB, which is around 100 minutes.",
|
||||||
"loggedIn": "Logged in as {{userName}}",
|
"loggedIn": "Logged in as {{userName}}",
|
||||||
"off": "Recording stopped",
|
"off": "Recording stopped",
|
||||||
"offBy": "{{name}} stopped the recording",
|
"offBy": "{{name}} stopped the recording",
|
||||||
@@ -894,6 +897,7 @@
|
|||||||
"onBy": "{{name}} started the recording",
|
"onBy": "{{name}} started the recording",
|
||||||
"pending": "Preparing to record the meeting...",
|
"pending": "Preparing to record the meeting...",
|
||||||
"rec": "REC",
|
"rec": "REC",
|
||||||
|
"saveLocalRecording": "Save recording file locally",
|
||||||
"serviceDescription": "Your recording will be saved by the recording service",
|
"serviceDescription": "Your recording will be saved by the recording service",
|
||||||
"serviceDescriptionCloud": "Cloud recording",
|
"serviceDescriptionCloud": "Cloud recording",
|
||||||
"serviceDescriptionCloudInfo": "Recorded meetings are automatically cleared 24h after their recording time.",
|
"serviceDescriptionCloudInfo": "Recorded meetings are automatically cleared 24h after their recording time.",
|
||||||
@@ -901,6 +905,7 @@
|
|||||||
"sessionAlreadyActive": "This session is already being recorded or live streamed.",
|
"sessionAlreadyActive": "This session is already being recorded or live streamed.",
|
||||||
"signIn": "Sign in",
|
"signIn": "Sign in",
|
||||||
"signOut": "Sign out",
|
"signOut": "Sign out",
|
||||||
|
"surfaceError": "Please select the current tab.",
|
||||||
"unavailable": "Oops! The {{serviceName}} is currently unavailable. We're working on resolving the issue. Please try again later.",
|
"unavailable": "Oops! The {{serviceName}} is currently unavailable. We're working on resolving the issue. Please try again later.",
|
||||||
"unavailableTitle": "Recording unavailable",
|
"unavailableTitle": "Recording unavailable",
|
||||||
"uploadToCloud": "Upload to the cloud"
|
"uploadToCloud": "Upload to the cloud"
|
||||||
|
|||||||
107
package-lock.json
generated
107
package-lock.json
generated
@@ -128,6 +128,7 @@
|
|||||||
"util": "0.12.1",
|
"util": "0.12.1",
|
||||||
"uuid": "8.3.2",
|
"uuid": "8.3.2",
|
||||||
"wasm-check": "2.0.1",
|
"wasm-check": "2.0.1",
|
||||||
|
"webm-duration-fix": "1.0.4",
|
||||||
"windows-iana": "^3.1.0",
|
"windows-iana": "^3.1.0",
|
||||||
"zxcvbn": "4.4.2"
|
"zxcvbn": "4.4.2"
|
||||||
},
|
},
|
||||||
@@ -141,6 +142,7 @@
|
|||||||
"@babel/runtime": "7.16.0",
|
"@babel/runtime": "7.16.0",
|
||||||
"@jitsi/eslint-config": "4.0.0",
|
"@jitsi/eslint-config": "4.0.0",
|
||||||
"@types/react-native": "0.67.6",
|
"@types/react-native": "0.67.6",
|
||||||
|
"@types/uuid": "8.3.4",
|
||||||
"babel-loader": "8.2.3",
|
"babel-loader": "8.2.3",
|
||||||
"babel-plugin-optional-require": "0.3.1",
|
"babel-plugin-optional-require": "0.3.1",
|
||||||
"circular-dependency-plugin": "5.2.0",
|
"circular-dependency-plugin": "5.2.0",
|
||||||
@@ -163,7 +165,7 @@
|
|||||||
"style-loader": "0.19.0",
|
"style-loader": "0.19.0",
|
||||||
"traverse": "0.6.6",
|
"traverse": "0.6.6",
|
||||||
"ts-loader": "9.2.6",
|
"ts-loader": "9.2.6",
|
||||||
"typescript": "4.3.5",
|
"typescript": "4.6.4",
|
||||||
"unorm": "1.6.0",
|
"unorm": "1.6.0",
|
||||||
"webpack": "5.57.1",
|
"webpack": "5.57.1",
|
||||||
"webpack-bundle-analyzer": "4.4.2",
|
"webpack-bundle-analyzer": "4.4.2",
|
||||||
@@ -5561,6 +5563,12 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/uuid": {
|
||||||
|
"version": "8.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz",
|
||||||
|
"integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/webgl-ext": {
|
"node_modules/@types/webgl-ext": {
|
||||||
"version": "0.0.30",
|
"version": "0.0.30",
|
||||||
"resolved": "https://registry.npmjs.org/@types/webgl-ext/-/webgl-ext-0.0.30.tgz",
|
"resolved": "https://registry.npmjs.org/@types/webgl-ext/-/webgl-ext-0.0.30.tgz",
|
||||||
@@ -8323,6 +8331,11 @@
|
|||||||
"integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==",
|
"integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/ebml-block": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ebml-block/-/ebml-block-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-HgNlIsRFP6D9VKU5atCeHRJY7XkJP8bOe8yEhd8NB7B3b4++VWTyauz6g650iiPmLfPLGlVpoJmGSgMfXDYusg=="
|
||||||
|
},
|
||||||
"node_modules/ee-first": {
|
"node_modules/ee-first": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||||
@@ -10657,6 +10670,14 @@
|
|||||||
"css-in-js-utils": "^2.0.0"
|
"css-in-js-utils": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/int64-buffer": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/int64-buffer/-/int64-buffer-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-+3azY4pXrjAupJHU1V9uGERWlhoqNswJNji6aD/02xac7oxol508AsMC5lxKhEqyZeDFy3enq5OGWXF4u75hiw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/internal-slot": {
|
"node_modules/internal-slot": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz",
|
||||||
@@ -18643,9 +18664,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "4.3.5",
|
"version": "4.6.4",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.4.tgz",
|
||||||
"integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==",
|
"integrity": "sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
@@ -19123,6 +19144,40 @@
|
|||||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||||
"integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE="
|
"integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE="
|
||||||
},
|
},
|
||||||
|
"node_modules/webm-duration-fix": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/webm-duration-fix/-/webm-duration-fix-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-kvhmSmEnuohtK+j+mJswqCCM2ViKb9W8Ch0oAxcaeUvpok5CsMORQLnea+CYKDXPG6JH12H0CbRK85qhfeZLew==",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer": "^6.0.3",
|
||||||
|
"ebml-block": "^1.1.2",
|
||||||
|
"events": "^3.3.0",
|
||||||
|
"int64-buffer": "^1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/webm-duration-fix/node_modules/buffer": {
|
||||||
|
"version": "6.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||||
|
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"base64-js": "^1.3.1",
|
||||||
|
"ieee754": "^1.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/webpack": {
|
"node_modules/webpack": {
|
||||||
"version": "5.57.1",
|
"version": "5.57.1",
|
||||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.57.1.tgz",
|
||||||
@@ -24162,6 +24217,12 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/uuid": {
|
||||||
|
"version": "8.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz",
|
||||||
|
"integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@types/webgl-ext": {
|
"@types/webgl-ext": {
|
||||||
"version": "0.0.30",
|
"version": "0.0.30",
|
||||||
"resolved": "https://registry.npmjs.org/@types/webgl-ext/-/webgl-ext-0.0.30.tgz",
|
"resolved": "https://registry.npmjs.org/@types/webgl-ext/-/webgl-ext-0.0.30.tgz",
|
||||||
@@ -26348,6 +26409,11 @@
|
|||||||
"integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==",
|
"integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"ebml-block": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ebml-block/-/ebml-block-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-HgNlIsRFP6D9VKU5atCeHRJY7XkJP8bOe8yEhd8NB7B3b4++VWTyauz6g650iiPmLfPLGlVpoJmGSgMfXDYusg=="
|
||||||
|
},
|
||||||
"ee-first": {
|
"ee-first": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||||
@@ -28173,6 +28239,11 @@
|
|||||||
"css-in-js-utils": "^2.0.0"
|
"css-in-js-utils": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"int64-buffer": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/int64-buffer/-/int64-buffer-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-+3azY4pXrjAupJHU1V9uGERWlhoqNswJNji6aD/02xac7oxol508AsMC5lxKhEqyZeDFy3enq5OGWXF4u75hiw=="
|
||||||
|
},
|
||||||
"internal-slot": {
|
"internal-slot": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz",
|
||||||
@@ -34274,9 +34345,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"typescript": {
|
"typescript": {
|
||||||
"version": "4.3.5",
|
"version": "4.6.4",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.4.tgz",
|
||||||
"integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==",
|
"integrity": "sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"ua-parser-js": {
|
"ua-parser-js": {
|
||||||
@@ -34621,6 +34692,28 @@
|
|||||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||||
"integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE="
|
"integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE="
|
||||||
},
|
},
|
||||||
|
"webm-duration-fix": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/webm-duration-fix/-/webm-duration-fix-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-kvhmSmEnuohtK+j+mJswqCCM2ViKb9W8Ch0oAxcaeUvpok5CsMORQLnea+CYKDXPG6JH12H0CbRK85qhfeZLew==",
|
||||||
|
"requires": {
|
||||||
|
"buffer": "^6.0.3",
|
||||||
|
"ebml-block": "^1.1.2",
|
||||||
|
"events": "^3.3.0",
|
||||||
|
"int64-buffer": "^1.0.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"buffer": {
|
||||||
|
"version": "6.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||||
|
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||||
|
"requires": {
|
||||||
|
"base64-js": "^1.3.1",
|
||||||
|
"ieee754": "^1.2.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"webpack": {
|
"webpack": {
|
||||||
"version": "5.57.1",
|
"version": "5.57.1",
|
||||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.57.1.tgz",
|
||||||
|
|||||||
@@ -133,6 +133,7 @@
|
|||||||
"util": "0.12.1",
|
"util": "0.12.1",
|
||||||
"uuid": "8.3.2",
|
"uuid": "8.3.2",
|
||||||
"wasm-check": "2.0.1",
|
"wasm-check": "2.0.1",
|
||||||
|
"webm-duration-fix": "1.0.4",
|
||||||
"windows-iana": "^3.1.0",
|
"windows-iana": "^3.1.0",
|
||||||
"zxcvbn": "4.4.2"
|
"zxcvbn": "4.4.2"
|
||||||
},
|
},
|
||||||
@@ -146,6 +147,7 @@
|
|||||||
"@babel/runtime": "7.16.0",
|
"@babel/runtime": "7.16.0",
|
||||||
"@jitsi/eslint-config": "4.0.0",
|
"@jitsi/eslint-config": "4.0.0",
|
||||||
"@types/react-native": "0.67.6",
|
"@types/react-native": "0.67.6",
|
||||||
|
"@types/uuid": "8.3.4",
|
||||||
"babel-loader": "8.2.3",
|
"babel-loader": "8.2.3",
|
||||||
"babel-plugin-optional-require": "0.3.1",
|
"babel-plugin-optional-require": "0.3.1",
|
||||||
"circular-dependency-plugin": "5.2.0",
|
"circular-dependency-plugin": "5.2.0",
|
||||||
@@ -168,7 +170,7 @@
|
|||||||
"style-loader": "0.19.0",
|
"style-loader": "0.19.0",
|
||||||
"traverse": "0.6.6",
|
"traverse": "0.6.6",
|
||||||
"ts-loader": "9.2.6",
|
"ts-loader": "9.2.6",
|
||||||
"typescript": "4.3.5",
|
"typescript": "4.6.4",
|
||||||
"unorm": "1.6.0",
|
"unorm": "1.6.0",
|
||||||
"webpack": "5.57.1",
|
"webpack": "5.57.1",
|
||||||
"webpack-bundle-analyzer": "4.4.2",
|
"webpack-bundle-analyzer": "4.4.2",
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ export default [
|
|||||||
'enableLayerSuspension',
|
'enableLayerSuspension',
|
||||||
'enableLipSync',
|
'enableLipSync',
|
||||||
'enableLobbyChat',
|
'enableLobbyChat',
|
||||||
|
'enableLocalRecording',
|
||||||
'enableOpusRed',
|
'enableOpusRed',
|
||||||
'enableRemb',
|
'enableRemb',
|
||||||
'enableSaveLogs',
|
'enableSaveLogs',
|
||||||
@@ -183,7 +184,6 @@ export default [
|
|||||||
'ignoreStartMuted',
|
'ignoreStartMuted',
|
||||||
'inviteAppName',
|
'inviteAppName',
|
||||||
'liveStreamingEnabled',
|
'liveStreamingEnabled',
|
||||||
'localRecording',
|
|
||||||
'localSubject',
|
'localSubject',
|
||||||
'maxFullResolutionParticipants',
|
'maxFullResolutionParticipants',
|
||||||
'mouseMoveCallbackInterval',
|
'mouseMoveCallbackInterval',
|
||||||
|
|||||||
@@ -231,3 +231,12 @@ export const OVERWRITE_PARTICIPANT_NAME = 'OVERWRITE_PARTICIPANT_NAME';
|
|||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
export const OVERWRITE_PARTICIPANTS_NAMES = 'OVERWRITE_PARTICIPANTS_NAMES';
|
export const OVERWRITE_PARTICIPANTS_NAMES = 'OVERWRITE_PARTICIPANTS_NAMES';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates participants local recording status.
|
||||||
|
* {
|
||||||
|
* type: SET_LOCAL_PARTICIPANT_RECORDING_STATUS,
|
||||||
|
* recording: boolean
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export const SET_LOCAL_PARTICIPANT_RECORDING_STATUS = 'SET_LOCAL_PARTICIPANT_RECORDING_STATUS';
|
||||||
|
|||||||
@@ -10,17 +10,18 @@ import {
|
|||||||
LOCAL_PARTICIPANT_AUDIO_LEVEL_CHANGED,
|
LOCAL_PARTICIPANT_AUDIO_LEVEL_CHANGED,
|
||||||
LOCAL_PARTICIPANT_RAISE_HAND,
|
LOCAL_PARTICIPANT_RAISE_HAND,
|
||||||
MUTE_REMOTE_PARTICIPANT,
|
MUTE_REMOTE_PARTICIPANT,
|
||||||
|
OVERWRITE_PARTICIPANT_NAME,
|
||||||
|
OVERWRITE_PARTICIPANTS_NAMES,
|
||||||
PARTICIPANT_ID_CHANGED,
|
PARTICIPANT_ID_CHANGED,
|
||||||
PARTICIPANT_JOINED,
|
PARTICIPANT_JOINED,
|
||||||
PARTICIPANT_KICKED,
|
PARTICIPANT_KICKED,
|
||||||
PARTICIPANT_LEFT,
|
PARTICIPANT_LEFT,
|
||||||
PARTICIPANT_UPDATED,
|
PARTICIPANT_UPDATED,
|
||||||
PIN_PARTICIPANT,
|
PIN_PARTICIPANT,
|
||||||
|
RAISE_HAND_UPDATED,
|
||||||
SCREENSHARE_PARTICIPANT_NAME_CHANGED,
|
SCREENSHARE_PARTICIPANT_NAME_CHANGED,
|
||||||
SET_LOADABLE_AVATAR_URL,
|
SET_LOADABLE_AVATAR_URL,
|
||||||
RAISE_HAND_UPDATED,
|
SET_LOCAL_PARTICIPANT_RECORDING_STATUS
|
||||||
OVERWRITE_PARTICIPANT_NAME,
|
|
||||||
OVERWRITE_PARTICIPANTS_NAMES
|
|
||||||
} from './actionTypes';
|
} from './actionTypes';
|
||||||
import {
|
import {
|
||||||
DISCO_REMOTE_CONTROL_FEATURE
|
DISCO_REMOTE_CONTROL_FEATURE
|
||||||
@@ -683,3 +684,19 @@ export function overwriteParticipantsNames(participantList) {
|
|||||||
participantList
|
participantList
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Local video recording status for the local participant.
|
||||||
|
*
|
||||||
|
* @param {boolean} recording - If local recording is ongoing.
|
||||||
|
* @returns {{
|
||||||
|
* type: SET_LOCAL_PARTICIPANT_RECORDING_STATUS,
|
||||||
|
* recording: boolean
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
export function updateLocalRecordingStatus(recording) {
|
||||||
|
return {
|
||||||
|
type: SET_LOCAL_PARTICIPANT_RECORDING_STATUS,
|
||||||
|
recording
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { getBreakoutRooms } from '../../breakout-rooms/functions';
|
|||||||
import { toggleE2EE } from '../../e2ee/actions';
|
import { toggleE2EE } from '../../e2ee/actions';
|
||||||
import { MAX_MODE } from '../../e2ee/constants';
|
import { MAX_MODE } from '../../e2ee/constants';
|
||||||
import {
|
import {
|
||||||
|
LOCAL_RECORDING_NOTIFICATION_ID,
|
||||||
NOTIFICATION_TIMEOUT_TYPE,
|
NOTIFICATION_TIMEOUT_TYPE,
|
||||||
RAISE_HAND_NOTIFICATION_ID,
|
RAISE_HAND_NOTIFICATION_ID,
|
||||||
showNotification
|
showNotification
|
||||||
@@ -17,6 +18,7 @@ import {
|
|||||||
import { isForceMuted } from '../../participants-pane/functions';
|
import { isForceMuted } from '../../participants-pane/functions';
|
||||||
import { CALLING, INVITED } from '../../presence-status';
|
import { CALLING, INVITED } from '../../presence-status';
|
||||||
import { RAISE_HAND_SOUND_ID } from '../../reactions/constants';
|
import { RAISE_HAND_SOUND_ID } from '../../reactions/constants';
|
||||||
|
import { RECORDING_OFF_SOUND_ID, RECORDING_ON_SOUND_ID } from '../../recording';
|
||||||
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../app';
|
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../app';
|
||||||
import {
|
import {
|
||||||
CONFERENCE_WILL_JOIN,
|
CONFERENCE_WILL_JOIN,
|
||||||
@@ -42,7 +44,8 @@ import {
|
|||||||
PARTICIPANT_JOINED,
|
PARTICIPANT_JOINED,
|
||||||
PARTICIPANT_LEFT,
|
PARTICIPANT_LEFT,
|
||||||
PARTICIPANT_UPDATED,
|
PARTICIPANT_UPDATED,
|
||||||
RAISE_HAND_UPDATED
|
RAISE_HAND_UPDATED,
|
||||||
|
SET_LOCAL_PARTICIPANT_RECORDING_STATUS
|
||||||
} from './actionTypes';
|
} from './actionTypes';
|
||||||
import {
|
import {
|
||||||
localParticipantIdChanged,
|
localParticipantIdChanged,
|
||||||
@@ -174,6 +177,25 @@ MiddlewareRegistry.register(store => next => action => {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case SET_LOCAL_PARTICIPANT_RECORDING_STATUS: {
|
||||||
|
const { recording } = action;
|
||||||
|
const localId = getLocalParticipant(store.getState())?.id;
|
||||||
|
|
||||||
|
store.dispatch(participantUpdated({
|
||||||
|
// XXX Only the local participant is allowed to update without
|
||||||
|
// stating the JitsiConference instance (i.e. participant property
|
||||||
|
// `conference` for a remote participant) because the local
|
||||||
|
// participant is uniquely identified by the very fact that there is
|
||||||
|
// only one local participant.
|
||||||
|
|
||||||
|
id: localId,
|
||||||
|
local: true,
|
||||||
|
localRecording: recording
|
||||||
|
}));
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case MUTE_REMOTE_PARTICIPANT: {
|
case MUTE_REMOTE_PARTICIPANT: {
|
||||||
const { conference } = store.getState()['features/base/conference'];
|
const { conference } = store.getState()['features/base/conference'];
|
||||||
|
|
||||||
@@ -389,6 +411,8 @@ StateListenerRegistry.register(
|
|||||||
id: participant.getId(),
|
id: participant.getId(),
|
||||||
features: { 'screen-sharing': true }
|
features: { 'screen-sharing': true }
|
||||||
})),
|
})),
|
||||||
|
'localRecording': (participant, value) =>
|
||||||
|
_localRecordingUpdated(store, conference, participant.getId(), value),
|
||||||
'raisedHand': (participant, value) =>
|
'raisedHand': (participant, value) =>
|
||||||
_raiseHandUpdated(store, conference, participant.getId(), value),
|
_raiseHandUpdated(store, conference, participant.getId(), value),
|
||||||
'region': (participant, value) =>
|
'region': (participant, value) =>
|
||||||
@@ -566,7 +590,15 @@ function _maybePlaySounds({ getState, dispatch }, action) {
|
|||||||
function _participantJoinedOrUpdated(store, next, action) {
|
function _participantJoinedOrUpdated(store, next, action) {
|
||||||
const { dispatch, getState } = store;
|
const { dispatch, getState } = store;
|
||||||
const { overwrittenNameList } = store.getState()['features/base/participants'];
|
const { overwrittenNameList } = store.getState()['features/base/participants'];
|
||||||
const { participant: { avatarURL, email, id, local, name, raisedHandTimestamp } } = action;
|
const { participant: {
|
||||||
|
avatarURL,
|
||||||
|
email,
|
||||||
|
id,
|
||||||
|
local,
|
||||||
|
localRecording,
|
||||||
|
name,
|
||||||
|
raisedHandTimestamp
|
||||||
|
} } = action;
|
||||||
|
|
||||||
// Send an external update of the local participant's raised hand state
|
// Send an external update of the local participant's raised hand state
|
||||||
// if a new raised hand state is defined in the action.
|
// if a new raised hand state is defined in the action.
|
||||||
@@ -587,6 +619,20 @@ function _participantJoinedOrUpdated(store, next, action) {
|
|||||||
action.participant.name = overwrittenNameList[id];
|
action.participant.name = overwrittenNameList[id];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Send an external update of the local participant's local recording state
|
||||||
|
// if a new local recording state is defined in the action.
|
||||||
|
if (typeof localRecording !== 'undefined') {
|
||||||
|
if (local) {
|
||||||
|
const conference = getCurrentConference(getState);
|
||||||
|
|
||||||
|
// Send localRecording signalling only if there is a change
|
||||||
|
if (conference
|
||||||
|
&& localRecording !== getLocalParticipant(getState()).localRecording) {
|
||||||
|
conference.setLocalParticipantProperty('localRecording', localRecording);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Allow the redux update to go through and compare the old avatar
|
// Allow the redux update to go through and compare the old avatar
|
||||||
// to the new avatar and emit out change events if necessary.
|
// to the new avatar and emit out change events if necessary.
|
||||||
const result = next(action);
|
const result = next(action);
|
||||||
@@ -618,6 +664,35 @@ function _participantJoinedOrUpdated(store, next, action) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles a local recording status update.
|
||||||
|
*
|
||||||
|
* @param {Function} dispatch - The Redux dispatch function.
|
||||||
|
* @param {Object} conference - The conference for which we got an update.
|
||||||
|
* @param {string} participantId - The ID of the participant from which we got an update.
|
||||||
|
* @param {boolean} newValue - The new value of the local recording status.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function _localRecordingUpdated({ dispatch, getState }, conference, participantId, newValue) {
|
||||||
|
const state = getState();
|
||||||
|
|
||||||
|
dispatch(participantUpdated({
|
||||||
|
conference,
|
||||||
|
id: participantId,
|
||||||
|
localRecording: newValue
|
||||||
|
}));
|
||||||
|
const participantName = getParticipantDisplayName(state, participantId);
|
||||||
|
|
||||||
|
dispatch(showNotification({
|
||||||
|
titleKey: 'notify.somebody',
|
||||||
|
title: participantName,
|
||||||
|
descriptionKey: newValue ? 'notify.localRecordingStarted' : 'notify.localRecordingStopped',
|
||||||
|
uid: LOCAL_RECORDING_NOTIFICATION_ID
|
||||||
|
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
|
||||||
|
dispatch(playSound(newValue ? RECORDING_ON_SOUND_ID : RECORDING_OFF_SOUND_ID));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles a raise hand status update.
|
* Handles a raise hand status update.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -58,13 +58,6 @@ export const NOTIFICATION_ICON = {
|
|||||||
PARTICIPANTS: 'participants'
|
PARTICIPANTS: 'participants'
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* The identifier of the salesforce link notification.
|
|
||||||
*
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
export const SALESFORCE_LINK_NOTIFICATION_ID = 'SALESFORCE_LINK_NOTIFICATION';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The identifier of the lobby notification.
|
* The identifier of the lobby notification.
|
||||||
*
|
*
|
||||||
@@ -72,6 +65,13 @@ export const SALESFORCE_LINK_NOTIFICATION_ID = 'SALESFORCE_LINK_NOTIFICATION';
|
|||||||
*/
|
*/
|
||||||
export const LOBBY_NOTIFICATION_ID = 'LOBBY_NOTIFICATION';
|
export const LOBBY_NOTIFICATION_ID = 'LOBBY_NOTIFICATION';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The identifier of the local recording notification.
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
export const LOCAL_RECORDING_NOTIFICATION_ID = 'LOCAL_RECORDING_NOTIFICATION_ID';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The identifier of the raise hand notification.
|
* The identifier of the raise hand notification.
|
||||||
*
|
*
|
||||||
@@ -79,6 +79,13 @@ export const LOBBY_NOTIFICATION_ID = 'LOBBY_NOTIFICATION';
|
|||||||
*/
|
*/
|
||||||
export const RAISE_HAND_NOTIFICATION_ID = 'RAISE_HAND_NOTIFICATION';
|
export const RAISE_HAND_NOTIFICATION_ID = 'RAISE_HAND_NOTIFICATION';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The identifier of the salesforce link notification.
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
export const SALESFORCE_LINK_NOTIFICATION_ID = 'SALESFORCE_LINK_NOTIFICATION';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Amount of participants beyond which no join notification will be emitted.
|
* Amount of participants beyond which no join notification will be emitted.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -66,3 +66,21 @@ export const SET_STREAM_KEY = 'SET_STREAM_KEY';
|
|||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
export const SET_MEETING_HIGHLIGHT_BUTTON_STATE = 'SET_MEETING_HIGHLIGHT_BUTTON_STATE';
|
export const SET_MEETING_HIGHLIGHT_BUTTON_STATE = 'SET_MEETING_HIGHLIGHT_BUTTON_STATE';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to start the local recording.
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* type: START_LOCAL_RECORDING
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export const START_LOCAL_RECORDING = 'START_LOCAL_RECORDING';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops local recording.
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* type: STOP_LOCAL_RECORDING
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export const STOP_LOCAL_RECORDING = 'STOP_LOCAL_RECORDING';
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ import {
|
|||||||
SET_MEETING_HIGHLIGHT_BUTTON_STATE,
|
SET_MEETING_HIGHLIGHT_BUTTON_STATE,
|
||||||
SET_PENDING_RECORDING_NOTIFICATION_UID,
|
SET_PENDING_RECORDING_NOTIFICATION_UID,
|
||||||
SET_SELECTED_RECORDING_SERVICE,
|
SET_SELECTED_RECORDING_SERVICE,
|
||||||
SET_STREAM_KEY
|
SET_STREAM_KEY,
|
||||||
|
START_LOCAL_RECORDING,
|
||||||
|
STOP_LOCAL_RECORDING
|
||||||
} from './actionTypes';
|
} from './actionTypes';
|
||||||
import {
|
import {
|
||||||
getRecordingLink,
|
getRecordingLink,
|
||||||
@@ -332,3 +334,25 @@ function _setPendingRecordingNotificationUid(uid: ?number, streamType: string) {
|
|||||||
uid
|
uid
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts local recording.
|
||||||
|
*
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
export function startLocalVideoRecording() {
|
||||||
|
return {
|
||||||
|
type: START_LOCAL_RECORDING
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops local recording.
|
||||||
|
*
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
export function stopLocalVideoRecording() {
|
||||||
|
return {
|
||||||
|
type: STOP_LOCAL_RECORDING
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import { maybeShowPremiumFeatureDialog } from '../../../jaas/actions';
|
|||||||
import { FEATURES } from '../../../jaas/constants';
|
import { FEATURES } from '../../../jaas/constants';
|
||||||
import { getActiveSession, getRecordButtonProps } from '../../functions';
|
import { getActiveSession, getRecordButtonProps } from '../../functions';
|
||||||
|
|
||||||
|
import LocalRecordingManager from './LocalRecordingManager';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The type of the React {@code Component} props of
|
* The type of the React {@code Component} props of
|
||||||
* {@link AbstractRecordButton}.
|
* {@link AbstractRecordButton}.
|
||||||
@@ -142,7 +144,8 @@ export function _mapStateToProps(state: Object): Object {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
_disabled,
|
_disabled,
|
||||||
_isRecordingRunning: Boolean(getActiveSession(state, JitsiRecordingConstants.mode.FILE)),
|
_isRecordingRunning: Boolean(getActiveSession(state, JitsiRecordingConstants.mode.FILE))
|
||||||
|
|| LocalRecordingManager.isRecordingLocally(),
|
||||||
_tooltip,
|
_tooltip,
|
||||||
visible
|
visible
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
} from '../../../dropbox';
|
} from '../../../dropbox';
|
||||||
import { NOTIFICATION_TIMEOUT_TYPE, showErrorNotification } from '../../../notifications';
|
import { NOTIFICATION_TIMEOUT_TYPE, showErrorNotification } from '../../../notifications';
|
||||||
import { toggleRequestingSubtitles } from '../../../subtitles';
|
import { toggleRequestingSubtitles } from '../../../subtitles';
|
||||||
import { setSelectedRecordingService } from '../../actions';
|
import { setSelectedRecordingService, startLocalVideoRecording } from '../../actions';
|
||||||
import { RECORDING_TYPES } from '../../constants';
|
import { RECORDING_TYPES } from '../../constants';
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
@@ -293,8 +293,9 @@ class AbstractStartRecordingDialog extends Component<Props, State> {
|
|||||||
let appData;
|
let appData;
|
||||||
const attributes = {};
|
const attributes = {};
|
||||||
|
|
||||||
if (_isDropboxEnabled && this.state.selectedRecordingService === RECORDING_TYPES.DROPBOX) {
|
switch (this.state.selectedRecordingService) {
|
||||||
if (_token) {
|
case RECORDING_TYPES.DROPBOX: {
|
||||||
|
if (_isDropboxEnabled && _token) {
|
||||||
appData = JSON.stringify({
|
appData = JSON.stringify({
|
||||||
'file_recording_metadata': {
|
'file_recording_metadata': {
|
||||||
'upload_credentials': {
|
'upload_credentials': {
|
||||||
@@ -313,13 +314,22 @@ class AbstractStartRecordingDialog extends Component<Props, State> {
|
|||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
break;
|
||||||
|
}
|
||||||
|
case RECORDING_TYPES.JITSI_REC_SERVICE: {
|
||||||
appData = JSON.stringify({
|
appData = JSON.stringify({
|
||||||
'file_recording_metadata': {
|
'file_recording_metadata': {
|
||||||
'share': this.state.sharingEnabled
|
'share': this.state.sharingEnabled
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
attributes.type = RECORDING_TYPES.JITSI_REC_SERVICE;
|
attributes.type = RECORDING_TYPES.JITSI_REC_SERVICE;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case RECORDING_TYPES.LOCAL: {
|
||||||
|
dispatch(startLocalVideoRecording());
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sendAnalytics(
|
sendAnalytics(
|
||||||
|
|||||||
@@ -7,8 +7,11 @@ import {
|
|||||||
sendAnalytics
|
sendAnalytics
|
||||||
} from '../../../analytics';
|
} from '../../../analytics';
|
||||||
import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
|
import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
|
||||||
|
import { stopLocalVideoRecording } from '../../actions';
|
||||||
import { getActiveSession } from '../../functions';
|
import { getActiveSession } from '../../functions';
|
||||||
|
|
||||||
|
import LocalRecordingManager from './LocalRecordingManager';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The type of the React {@code Component} props of
|
* The type of the React {@code Component} props of
|
||||||
* {@link AbstractStopRecordingDialog}.
|
* {@link AbstractStopRecordingDialog}.
|
||||||
@@ -25,6 +28,11 @@ export type Props = {
|
|||||||
*/
|
*/
|
||||||
_fileRecordingSession: Object,
|
_fileRecordingSession: Object,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the recording is a local recording or not.
|
||||||
|
*/
|
||||||
|
_localRecording: boolean,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The redux dispatch function.
|
* The redux dispatch function.
|
||||||
*/
|
*/
|
||||||
@@ -68,11 +76,15 @@ export default class AbstractStopRecordingDialog<P: Props>
|
|||||||
_onSubmit() {
|
_onSubmit() {
|
||||||
sendAnalytics(createRecordingDialogEvent('stop', 'confirm.button'));
|
sendAnalytics(createRecordingDialogEvent('stop', 'confirm.button'));
|
||||||
|
|
||||||
const { _fileRecordingSession } = this.props;
|
if (this.props._localRecording) {
|
||||||
|
this.props.dispatch(stopLocalVideoRecording());
|
||||||
|
} else {
|
||||||
|
const { _fileRecordingSession } = this.props;
|
||||||
|
|
||||||
if (_fileRecordingSession) {
|
if (_fileRecordingSession) {
|
||||||
this.props._conference.stopRecording(_fileRecordingSession.id);
|
this.props._conference.stopRecording(_fileRecordingSession.id);
|
||||||
this._toggleScreenshotCapture();
|
this._toggleScreenshotCapture();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -105,6 +117,7 @@ export function _mapStateToProps(state: Object) {
|
|||||||
return {
|
return {
|
||||||
_conference: state['features/base/conference'].conference,
|
_conference: state['features/base/conference'].conference,
|
||||||
_fileRecordingSession:
|
_fileRecordingSession:
|
||||||
getActiveSession(state, JitsiRecordingConstants.mode.FILE)
|
getActiveSession(state, JitsiRecordingConstants.mode.FILE),
|
||||||
|
_localRecording: LocalRecordingManager.isRecordingLocally()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,221 @@
|
|||||||
|
import { v4 as uuidV4 } from 'uuid';
|
||||||
|
import fixWebmDuration from 'webm-duration-fix';
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import { getRoomName } from '../../../base/conference';
|
||||||
|
// @ts-ignore
|
||||||
|
import { MEDIA_TYPE } from '../../../base/media';
|
||||||
|
// @ts-ignore
|
||||||
|
import { getTrackState } from '../../../base/tracks';
|
||||||
|
// @ts-ignore
|
||||||
|
import { stopLocalVideoRecording } from '../../actions.any';
|
||||||
|
|
||||||
|
interface IReduxStore {
|
||||||
|
dispatch: Function;
|
||||||
|
getState: Function;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ILocalRecordingManager {
|
||||||
|
recordingData: Blob[];
|
||||||
|
recorder: MediaRecorder|undefined;
|
||||||
|
stream: MediaStream|undefined;
|
||||||
|
audioContext: AudioContext|undefined;
|
||||||
|
audioDestination: MediaStreamAudioDestinationNode|undefined;
|
||||||
|
roomName: string;
|
||||||
|
mediaType: string;
|
||||||
|
initializeAudioMixer: () => void;
|
||||||
|
mixAudioStream: (stream: MediaStream) => void;
|
||||||
|
addAudioTrackToLocalRecording: (track: MediaStreamTrack) => void;
|
||||||
|
getFilename: () => string;
|
||||||
|
saveRecording: (recordingData: Blob[], filename: string) => void;
|
||||||
|
stopLocalRecording: () => void;
|
||||||
|
startLocalRecording: (store: IReduxStore) => void;
|
||||||
|
isRecordingLocally: () => boolean;
|
||||||
|
totalSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getMimeType = (): string => {
|
||||||
|
const possibleTypes = [
|
||||||
|
'video/mp4;codecs=h264',
|
||||||
|
'video/webm;codecs=h264',
|
||||||
|
'video/webm;codecs=vp9',
|
||||||
|
'video/webm;codecs=vp8',
|
||||||
|
];
|
||||||
|
for(let type of possibleTypes) {
|
||||||
|
if(MediaRecorder.isTypeSupported(type)) {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error("No MIME Type supported by MediaRecorder");
|
||||||
|
}
|
||||||
|
|
||||||
|
const VIDEO_BIT_RATE = 2500000; // 2.5Mbps in bits
|
||||||
|
|
||||||
|
const LocalRecordingManager: ILocalRecordingManager = {
|
||||||
|
recordingData: [],
|
||||||
|
recorder: undefined,
|
||||||
|
stream: undefined,
|
||||||
|
audioContext: undefined,
|
||||||
|
audioDestination: undefined,
|
||||||
|
roomName: '',
|
||||||
|
mediaType: getMimeType(),
|
||||||
|
totalSize: 1073741824, // 1GB in bytes
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes audio context used for mixing audio tracks.
|
||||||
|
*/
|
||||||
|
initializeAudioMixer() {
|
||||||
|
this.audioContext = new AudioContext();
|
||||||
|
this.audioDestination = this.audioContext.createMediaStreamDestination();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mixes multiple audio tracks to the destination media stream.
|
||||||
|
* */
|
||||||
|
mixAudioStream(stream) {
|
||||||
|
if (stream.getAudioTracks().length > 0 && this.audioDestination) {
|
||||||
|
this.audioContext?.createMediaStreamSource(stream).connect(this.audioDestination);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds audio track to the recording stream.
|
||||||
|
*/
|
||||||
|
addAudioTrackToLocalRecording(track) {
|
||||||
|
if (track) {
|
||||||
|
const stream = new MediaStream([ track ]);
|
||||||
|
|
||||||
|
this.mixAudioStream(stream);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a filename based ono the Jitsi room name in the URL and timestamp.
|
||||||
|
* */
|
||||||
|
getFilename() {
|
||||||
|
const now = new Date();
|
||||||
|
const timestamp = now.toISOString();
|
||||||
|
|
||||||
|
return `${this.roomName}_${timestamp}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves local recording to file.
|
||||||
|
* */
|
||||||
|
async saveRecording(recordingData, filename) {
|
||||||
|
// @ts-ignore
|
||||||
|
const blob = await fixWebmDuration(new Blob(recordingData, { type: this.mediaType }));
|
||||||
|
// @ts-ignore
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
|
||||||
|
const extension = this.mediaType.slice(this.mediaType.indexOf('/') + 1, this.mediaType.indexOf(';'))
|
||||||
|
a.style.display = 'none';
|
||||||
|
a.href = url;
|
||||||
|
a.download = `${filename}.${extension}`;
|
||||||
|
a.click();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops local recording.
|
||||||
|
* */
|
||||||
|
stopLocalRecording() {
|
||||||
|
if (this.recorder) {
|
||||||
|
this.recorder.stop();
|
||||||
|
this.recorder = undefined;
|
||||||
|
this.audioContext = undefined;
|
||||||
|
this.audioDestination = undefined;
|
||||||
|
setTimeout(() => this.saveRecording(this.recordingData, this.getFilename()), 1000);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a local recording.
|
||||||
|
*/
|
||||||
|
async startLocalRecording(store) {
|
||||||
|
const { dispatch, getState } = store;
|
||||||
|
// @ts-ignore
|
||||||
|
const supportsCaptureHandle = Boolean(navigator.mediaDevices.setCaptureHandleConfig);
|
||||||
|
const tabId = uuidV4();
|
||||||
|
|
||||||
|
if (supportsCaptureHandle) {
|
||||||
|
// @ts-ignore
|
||||||
|
navigator.mediaDevices.setCaptureHandleConfig({
|
||||||
|
handle: `JitsiMeet-${tabId}`,
|
||||||
|
permittedOrigins: [ '*' ]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.recordingData = [];
|
||||||
|
// @ts-ignore
|
||||||
|
const gdmStream = await navigator.mediaDevices.getDisplayMedia({
|
||||||
|
// @ts-ignore
|
||||||
|
video: { displaySurface: 'browser' },
|
||||||
|
audio: true
|
||||||
|
});
|
||||||
|
// @ts-ignore
|
||||||
|
const isBrowser = gdmStream.getVideoTracks()[0].getSettings().displaySurface === 'browser';
|
||||||
|
|
||||||
|
if (!isBrowser || (supportsCaptureHandle // @ts-ignore
|
||||||
|
&& gdmStream.getVideoTracks()[0].getCaptureHandle()?.handle !== `JitsiMeet-${tabId}`)) {
|
||||||
|
gdmStream.getTracks().forEach((track: MediaStreamTrack) => track.stop());
|
||||||
|
throw new Error('WrongSurfaceSelected');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.initializeAudioMixer();
|
||||||
|
this.mixAudioStream(gdmStream);
|
||||||
|
this.roomName = getRoomName(getState());
|
||||||
|
const tracks = getTrackState(getState());
|
||||||
|
|
||||||
|
tracks.forEach((track: any) => {
|
||||||
|
if (track.mediaType === MEDIA_TYPE.AUDIO) {
|
||||||
|
const audioTrack = track?.jitsiTrack?.track;
|
||||||
|
|
||||||
|
this.addAudioTrackToLocalRecording(audioTrack);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.stream = new MediaStream([
|
||||||
|
...(this.audioDestination?.stream.getAudioTracks() || []),
|
||||||
|
gdmStream.getVideoTracks()[0]
|
||||||
|
]);
|
||||||
|
this.recorder = new MediaRecorder(this.stream, {
|
||||||
|
mimeType: this.mediaType,
|
||||||
|
videoBitsPerSecond: VIDEO_BIT_RATE
|
||||||
|
});
|
||||||
|
this.recorder.addEventListener('dataavailable', e => {
|
||||||
|
if (e.data && e.data.size > 0) {
|
||||||
|
this.recordingData.push(e.data);
|
||||||
|
this.totalSize -= e.data.size;
|
||||||
|
if (this.totalSize <= 0) {
|
||||||
|
this.stopLocalRecording();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.recorder.addEventListener('stop', () => {
|
||||||
|
this.stream?.getTracks().forEach((track: MediaStreamTrack) => track.stop());
|
||||||
|
gdmStream.getTracks().forEach((track: MediaStreamTrack) => track.stop());
|
||||||
|
});
|
||||||
|
|
||||||
|
gdmStream.addEventListener('inactive', () => {
|
||||||
|
dispatch(stopLocalVideoRecording());
|
||||||
|
});
|
||||||
|
|
||||||
|
this.stream.addEventListener('inactive', () => {
|
||||||
|
dispatch(stopLocalVideoRecording());
|
||||||
|
});
|
||||||
|
|
||||||
|
this.recorder.start(5000);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not we're currently recording locally.
|
||||||
|
*/
|
||||||
|
isRecordingLocally() {
|
||||||
|
return Boolean(this.recorder);
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LocalRecordingManager;
|
||||||
@@ -10,7 +10,9 @@ import { ColorSchemeRegistry } from '../../../base/color-scheme';
|
|||||||
import {
|
import {
|
||||||
_abstractMapStateToProps
|
_abstractMapStateToProps
|
||||||
} from '../../../base/dialog';
|
} from '../../../base/dialog';
|
||||||
|
import { isMobileBrowser } from '../../../base/environment/utils';
|
||||||
import { translate } from '../../../base/i18n';
|
import { translate } from '../../../base/i18n';
|
||||||
|
import { browser } from '../../../base/lib-jitsi-meet';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Container,
|
Container,
|
||||||
@@ -31,6 +33,7 @@ import {
|
|||||||
ICON_CLOUD,
|
ICON_CLOUD,
|
||||||
ICON_INFO,
|
ICON_INFO,
|
||||||
ICON_USERS,
|
ICON_USERS,
|
||||||
|
LOCAL_RECORDING,
|
||||||
TRACK_COLOR
|
TRACK_COLOR
|
||||||
} from './styles';
|
} from './styles';
|
||||||
|
|
||||||
@@ -41,6 +44,11 @@ type Props = {
|
|||||||
*/
|
*/
|
||||||
_dialogStyles: StyleType,
|
_dialogStyles: StyleType,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether local recording is enabled or not.
|
||||||
|
*/
|
||||||
|
_localRecordingEnabled: boolean,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The color-schemed stylesheet of this component.
|
* The color-schemed stylesheet of this component.
|
||||||
*/
|
*/
|
||||||
@@ -126,6 +134,8 @@ type Props = {
|
|||||||
* @augments Component
|
* @augments Component
|
||||||
*/
|
*/
|
||||||
class StartRecordingDialogContent extends Component<Props> {
|
class StartRecordingDialogContent extends Component<Props> {
|
||||||
|
_localRecordingAvailable: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes a new {@code StartRecordingDialogContent} instance.
|
* Initializes a new {@code StartRecordingDialogContent} instance.
|
||||||
*
|
*
|
||||||
@@ -133,12 +143,29 @@ class StartRecordingDialogContent extends Component<Props> {
|
|||||||
*/
|
*/
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
const supportsLocalRecording = browser.isChromiumBased() && !browser.isElectron() && !isMobileBrowser();
|
||||||
|
|
||||||
|
this._localRecordingAvailable = props._localRecordingEnabled && supportsLocalRecording;
|
||||||
|
|
||||||
// Bind event handler so it is only bound once for every instance.
|
// Bind event handler so it is only bound once for every instance.
|
||||||
this._onSignIn = this._onSignIn.bind(this);
|
this._onSignIn = this._onSignIn.bind(this);
|
||||||
this._onSignOut = this._onSignOut.bind(this);
|
this._onSignOut = this._onSignOut.bind(this);
|
||||||
this._onDropboxSwitchChange = this._onDropboxSwitchChange.bind(this);
|
this._onDropboxSwitchChange = this._onDropboxSwitchChange.bind(this);
|
||||||
this._onRecordingServiceSwitchChange = this._onRecordingServiceSwitchChange.bind(this);
|
this._onRecordingServiceSwitchChange = this._onRecordingServiceSwitchChange.bind(this);
|
||||||
|
this._onLocalRecordingSwitchChange = this._onLocalRecordingSwitchChange.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements the Component's componentDidMount method.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
componentDidMount() {
|
||||||
|
if (!this._shouldRenderNoIntegrationsContent()
|
||||||
|
&& !this._shouldRenderIntegrationsContent()
|
||||||
|
&& !this._shouldRenderFileSharingContent()) {
|
||||||
|
this._onLocalRecordingSwitchChange();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -158,21 +185,35 @@ class StartRecordingDialogContent extends Component<Props> {
|
|||||||
{ this._renderFileSharingContent() }
|
{ this._renderFileSharingContent() }
|
||||||
{ this._renderUploadToTheCloudInfo() }
|
{ this._renderUploadToTheCloudInfo() }
|
||||||
{ this._renderIntegrationsContent() }
|
{ this._renderIntegrationsContent() }
|
||||||
|
{ this._renderLocalRecordingContent() }
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the file sharing content should be rendered or not.
|
||||||
|
*
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
_shouldRenderFileSharingContent() {
|
||||||
|
const { fileRecordingsServiceSharingEnabled, isVpaas, selectedRecordingService } = this.props;
|
||||||
|
|
||||||
|
if (!fileRecordingsServiceSharingEnabled
|
||||||
|
|| isVpaas
|
||||||
|
|| selectedRecordingService !== RECORDING_TYPES.JITSI_REC_SERVICE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the file recording service sharing options, if enabled.
|
* Renders the file recording service sharing options, if enabled.
|
||||||
*
|
*
|
||||||
* @returns {React$Component}
|
* @returns {React$Component}
|
||||||
*/
|
*/
|
||||||
_renderFileSharingContent() {
|
_renderFileSharingContent() {
|
||||||
const { fileRecordingsServiceSharingEnabled, isVpaas, selectedRecordingService } = this.props;
|
if (!this._shouldRenderFileSharingContent()) {
|
||||||
|
|
||||||
if (!fileRecordingsServiceSharingEnabled
|
|
||||||
|| isVpaas
|
|
||||||
|| selectedRecordingService !== RECORDING_TYPES.JITSI_REC_SERVICE) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,24 +296,36 @@ class StartRecordingDialogContent extends Component<Props> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the no integrations content should be rendered or not.
|
||||||
|
*
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
_shouldRenderNoIntegrationsContent() {
|
||||||
|
// show the non integrations part only if fileRecordingsServiceEnabled
|
||||||
|
// is enabled or when there are no integrations enabled
|
||||||
|
if (!(this.props.fileRecordingsServiceEnabled
|
||||||
|
|| !this.props.integrationsEnabled)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the content in case no integrations were enabled.
|
* Renders the content in case no integrations were enabled.
|
||||||
*
|
*
|
||||||
* @returns {React$Component}
|
* @returns {React$Component}
|
||||||
*/
|
*/
|
||||||
_renderNoIntegrationsContent() {
|
_renderNoIntegrationsContent() {
|
||||||
|
if (!this._shouldRenderNoIntegrationsContent()) {
|
||||||
// show the non integrations part only if fileRecordingsServiceEnabled
|
|
||||||
// is enabled or when there are no integrations enabled
|
|
||||||
if (!(this.props.fileRecordingsServiceEnabled
|
|
||||||
|| !this.props.integrationsEnabled)) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { _dialogStyles, _styles: styles, isValidating, isVpaas, t } = this.props;
|
const { _dialogStyles, _styles: styles, isValidating, isVpaas, t } = this.props;
|
||||||
|
|
||||||
const switchContent
|
const switchContent
|
||||||
= this.props.integrationsEnabled
|
= this.props.integrationsEnabled || this.props._localRecordingEnabled
|
||||||
? (
|
? (
|
||||||
<Switch
|
<Switch
|
||||||
className = 'recording-switch'
|
className = 'recording-switch'
|
||||||
@@ -285,7 +338,7 @@ class StartRecordingDialogContent extends Component<Props> {
|
|||||||
|
|
||||||
const label = isVpaas ? t('recording.serviceDescriptionCloud') : t('recording.serviceDescription');
|
const label = isVpaas ? t('recording.serviceDescriptionCloud') : t('recording.serviceDescription');
|
||||||
const jitsiContentRecordingIconContainer
|
const jitsiContentRecordingIconContainer
|
||||||
= this.props.integrationsEnabled
|
= this.props.integrationsEnabled || this.props._localRecordingEnabled
|
||||||
? 'jitsi-content-recording-icon-container-with-switch'
|
? 'jitsi-content-recording-icon-container-with-switch'
|
||||||
: 'jitsi-content-recording-icon-container-without-switch';
|
: 'jitsi-content-recording-icon-container-without-switch';
|
||||||
const contentRecordingClass = isVpaas
|
const contentRecordingClass = isVpaas
|
||||||
@@ -317,6 +370,19 @@ class StartRecordingDialogContent extends Component<Props> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the integrations content should be rendered or not.
|
||||||
|
*
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
_shouldRenderIntegrationsContent() {
|
||||||
|
if (!this.props.integrationsEnabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the content in case integrations were enabled.
|
* Renders the content in case integrations were enabled.
|
||||||
*
|
*
|
||||||
@@ -324,7 +390,7 @@ class StartRecordingDialogContent extends Component<Props> {
|
|||||||
* @returns {React$Component}
|
* @returns {React$Component}
|
||||||
*/
|
*/
|
||||||
_renderIntegrationsContent() {
|
_renderIntegrationsContent() {
|
||||||
if (!this.props.integrationsEnabled) {
|
if (!this._shouldRenderIntegrationsContent()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,7 +442,7 @@ class StartRecordingDialogContent extends Component<Props> {
|
|||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Container
|
<Container
|
||||||
className = 'recording-header recording-header-line'
|
className = 'recording-header'
|
||||||
style = { styles.headerIntegrations }>
|
style = { styles.headerIntegrations }>
|
||||||
<Container
|
<Container
|
||||||
className = 'recording-icon-container'>
|
className = 'recording-icon-container'>
|
||||||
@@ -405,6 +471,7 @@ class StartRecordingDialogContent extends Component<Props> {
|
|||||||
|
|
||||||
_onDropboxSwitchChange: () => void;
|
_onDropboxSwitchChange: () => void;
|
||||||
_onRecordingServiceSwitchChange: () => void;
|
_onRecordingServiceSwitchChange: () => void;
|
||||||
|
_onLocalRecordingSwitchChange: () => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handler for onValueChange events from the Switch component.
|
* Handler for onValueChange events from the Switch component.
|
||||||
@@ -419,8 +486,7 @@ class StartRecordingDialogContent extends Component<Props> {
|
|||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
// act like group, cannot toggle off
|
// act like group, cannot toggle off
|
||||||
if (selectedRecordingService
|
if (selectedRecordingService === RECORDING_TYPES.JITSI_REC_SERVICE) {
|
||||||
=== RECORDING_TYPES.JITSI_REC_SERVICE) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -444,8 +510,7 @@ class StartRecordingDialogContent extends Component<Props> {
|
|||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
// act like group, cannot toggle off
|
// act like group, cannot toggle off
|
||||||
if (selectedRecordingService
|
if (selectedRecordingService === RECORDING_TYPES.DROPBOX) {
|
||||||
=== RECORDING_TYPES.DROPBOX) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -456,6 +521,30 @@ class StartRecordingDialogContent extends Component<Props> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for onValueChange events from the Switch component.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onLocalRecordingSwitchChange() {
|
||||||
|
const {
|
||||||
|
onChange,
|
||||||
|
selectedRecordingService
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (!this._localRecordingAvailable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// act like group, cannot toggle off
|
||||||
|
if (selectedRecordingService
|
||||||
|
=== RECORDING_TYPES.LOCAL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange(RECORDING_TYPES.LOCAL);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders a spinner component.
|
* Renders a spinner component.
|
||||||
*
|
*
|
||||||
@@ -511,6 +600,60 @@ class StartRecordingDialogContent extends Component<Props> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_renderLocalRecordingContent: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the content for local recordings.
|
||||||
|
*
|
||||||
|
* @protected
|
||||||
|
* @returns {React$Component}
|
||||||
|
*/
|
||||||
|
_renderLocalRecordingContent() {
|
||||||
|
const { _styles: styles, isValidating, t, _dialogStyles, selectedRecordingService } = this.props;
|
||||||
|
|
||||||
|
if (!this._localRecordingAvailable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Container
|
||||||
|
className = 'recording-header recording-header-line'
|
||||||
|
style = { styles.header }>
|
||||||
|
<Container
|
||||||
|
className = 'recording-icon-container'>
|
||||||
|
<Image
|
||||||
|
className = 'recording-icon'
|
||||||
|
src = { LOCAL_RECORDING }
|
||||||
|
style = { styles.recordingIcon } />
|
||||||
|
</Container>
|
||||||
|
<Text
|
||||||
|
className = 'recording-title'
|
||||||
|
style = {{
|
||||||
|
..._dialogStyles.text,
|
||||||
|
...styles.title
|
||||||
|
}}>
|
||||||
|
{ t('recording.saveLocalRecording') }
|
||||||
|
</Text>
|
||||||
|
<Switch
|
||||||
|
className = 'recording-switch'
|
||||||
|
disabled = { isValidating }
|
||||||
|
onValueChange = { this._onLocalRecordingSwitchChange }
|
||||||
|
style = { styles.switch }
|
||||||
|
trackColor = {{ false: TRACK_COLOR }}
|
||||||
|
value = { this.props.selectedRecordingService
|
||||||
|
=== RECORDING_TYPES.LOCAL } />
|
||||||
|
</Container>
|
||||||
|
{selectedRecordingService === RECORDING_TYPES.LOCAL
|
||||||
|
&& <Text className = 'local-recording-warning'>
|
||||||
|
{t('recording.localRecordingWarning')}
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
</Container>
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
_onSignIn: () => void;
|
_onSignIn: () => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -546,6 +689,7 @@ function _mapStateToProps(state) {
|
|||||||
return {
|
return {
|
||||||
..._abstractMapStateToProps(state),
|
..._abstractMapStateToProps(state),
|
||||||
isVpaas: isVpaasMeeting(state),
|
isVpaas: isVpaasMeeting(state),
|
||||||
|
_localRecordingEnabled: state['features/base/config'].enableLocalRecording,
|
||||||
_styles: ColorSchemeRegistry.get(state, 'StartRecordingDialogContent')
|
_styles: ColorSchemeRegistry.get(state, 'StartRecordingDialogContent')
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export const DROPBOX_LOGO = require('../../../../../images/dropboxLogo_square.pn
|
|||||||
export const ICON_CLOUD = require('../../../../../images/icon-cloud.png');
|
export const ICON_CLOUD = require('../../../../../images/icon-cloud.png');
|
||||||
export const ICON_INFO = require('../../../../../images/icon-info.png');
|
export const ICON_INFO = require('../../../../../images/icon-info.png');
|
||||||
export const ICON_USERS = require('../../../../../images/icon-users.png');
|
export const ICON_USERS = require('../../../../../images/icon-users.png');
|
||||||
|
export const LOCAL_RECORDING = require('../../../../../images/downloadLocalRecording.png');
|
||||||
export const TRACK_COLOR = BaseTheme.palette.ui15;
|
export const TRACK_COLOR = BaseTheme.palette.ui15;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ export default {};
|
|||||||
|
|
||||||
export const DROPBOX_LOGO = 'images/dropboxLogo_square.png';
|
export const DROPBOX_LOGO = 'images/dropboxLogo_square.png';
|
||||||
|
|
||||||
|
export const LOCAL_RECORDING = 'images/downloadLocalRecording.png';
|
||||||
|
|
||||||
export const ICON_CLOUD = 'images/icon-cloud.png';
|
export const ICON_CLOUD = 'images/icon-cloud.png';
|
||||||
|
|
||||||
export const ICON_INFO = 'images/icon-info.png';
|
export const ICON_INFO = 'images/icon-info.png';
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ class StartRecordingDialog extends AbstractStartRecordingDialog {
|
|||||||
return false;
|
return false;
|
||||||
} else if (selectedRecordingService === RECORDING_TYPES.DROPBOX) {
|
} else if (selectedRecordingService === RECORDING_TYPES.DROPBOX) {
|
||||||
return !isTokenValid;
|
return !isTokenValid;
|
||||||
|
} else if (selectedRecordingService === RECORDING_TYPES.LOCAL) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -45,7 +45,8 @@ export const RECORDING_ON_SOUND_ID = 'RECORDING_ON_SOUND';
|
|||||||
*/
|
*/
|
||||||
export const RECORDING_TYPES = {
|
export const RECORDING_TYPES = {
|
||||||
JITSI_REC_SERVICE: 'recording-service',
|
JITSI_REC_SERVICE: 'recording-service',
|
||||||
DROPBOX: 'dropbox'
|
DROPBOX: 'dropbox',
|
||||||
|
LOCAL: 'local'
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import { JitsiRecordingConstants } from '../base/lib-jitsi-meet';
|
import { JitsiRecordingConstants } from '../base/lib-jitsi-meet';
|
||||||
import { getLocalParticipant, isLocalParticipantModerator } from '../base/participants';
|
import { getLocalParticipant, getRemoteParticipants, isLocalParticipantModerator } from '../base/participants';
|
||||||
import { isInBreakoutRoom } from '../breakout-rooms/functions';
|
import { isInBreakoutRoom } from '../breakout-rooms/functions';
|
||||||
import { isEnabled as isDropboxEnabled } from '../dropbox';
|
import { isEnabled as isDropboxEnabled } from '../dropbox';
|
||||||
import { extractFqnFromPath } from '../dynamic-branding/functions.any';
|
import { extractFqnFromPath } from '../dynamic-branding/functions.any';
|
||||||
|
|
||||||
|
import LocalRecordingManager from './components/Recording/LocalRecordingManager';
|
||||||
import { RECORDING_STATUS_PRIORITIES, RECORDING_TYPES } from './constants';
|
import { RECORDING_STATUS_PRIORITIES, RECORDING_TYPES } from './constants';
|
||||||
import logger from './logger';
|
import logger from './logger';
|
||||||
|
|
||||||
@@ -116,6 +117,11 @@ export function getSessionStatusToShow(state: Object, mode: string): ?string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if ((!Array.isArray(recordingSessions) || recordingSessions.length === 0)
|
||||||
|
&& mode === JitsiRecordingConstants.mode.FILE
|
||||||
|
&& (LocalRecordingManager.isRecordingLocally() || isRemoteParticipantRecordingLocally(state))) {
|
||||||
|
status = JitsiRecordingConstants.status.ON;
|
||||||
|
}
|
||||||
|
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
@@ -241,3 +247,22 @@ export async function sendMeetingHighlight(state: Object) {
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether a remote participant is recording locally or not.
|
||||||
|
*
|
||||||
|
* @param {Object} state - Redux state.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function isRemoteParticipantRecordingLocally(state) {
|
||||||
|
const participants = getRemoteParticipants(state);
|
||||||
|
|
||||||
|
// eslint-disable-next-line prefer-const
|
||||||
|
for (let value of participants.values()) {
|
||||||
|
if (value.localRecording) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import JitsiMeetJS, {
|
|||||||
JitsiConferenceEvents,
|
JitsiConferenceEvents,
|
||||||
JitsiRecordingConstants
|
JitsiRecordingConstants
|
||||||
} from '../base/lib-jitsi-meet';
|
} from '../base/lib-jitsi-meet';
|
||||||
import { getParticipantDisplayName } from '../base/participants';
|
import { MEDIA_TYPE } from '../base/media';
|
||||||
|
import { getParticipantDisplayName, updateLocalRecordingStatus } from '../base/participants';
|
||||||
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
|
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
|
||||||
import {
|
import {
|
||||||
playSound,
|
playSound,
|
||||||
@@ -19,8 +20,10 @@ import {
|
|||||||
stopSound,
|
stopSound,
|
||||||
unregisterSound
|
unregisterSound
|
||||||
} from '../base/sounds';
|
} from '../base/sounds';
|
||||||
|
import { TRACK_ADDED } from '../base/tracks';
|
||||||
|
import { NOTIFICATION_TIMEOUT_TYPE, showErrorNotification, showNotification } from '../notifications';
|
||||||
|
|
||||||
import { RECORDING_SESSION_UPDATED } from './actionTypes';
|
import { RECORDING_SESSION_UPDATED, START_LOCAL_RECORDING, STOP_LOCAL_RECORDING } from './actionTypes';
|
||||||
import {
|
import {
|
||||||
clearRecordingSessions,
|
clearRecordingSessions,
|
||||||
hidePendingRecordingNotification,
|
hidePendingRecordingNotification,
|
||||||
@@ -32,13 +35,18 @@ import {
|
|||||||
showStoppedRecordingNotification,
|
showStoppedRecordingNotification,
|
||||||
updateRecordingSessionData
|
updateRecordingSessionData
|
||||||
} from './actions';
|
} from './actions';
|
||||||
|
import LocalRecordingManager from './components/Recording/LocalRecordingManager';
|
||||||
import {
|
import {
|
||||||
LIVE_STREAMING_OFF_SOUND_ID,
|
LIVE_STREAMING_OFF_SOUND_ID,
|
||||||
LIVE_STREAMING_ON_SOUND_ID,
|
LIVE_STREAMING_ON_SOUND_ID,
|
||||||
RECORDING_OFF_SOUND_ID,
|
RECORDING_OFF_SOUND_ID,
|
||||||
RECORDING_ON_SOUND_ID
|
RECORDING_ON_SOUND_ID
|
||||||
} from './constants';
|
} from './constants';
|
||||||
import { getSessionById, getResourceId } from './functions';
|
import {
|
||||||
|
getSessionById,
|
||||||
|
getResourceId
|
||||||
|
} from './functions';
|
||||||
|
import logger from './logger';
|
||||||
import {
|
import {
|
||||||
LIVE_STREAMING_OFF_SOUND_FILE,
|
LIVE_STREAMING_OFF_SOUND_FILE,
|
||||||
LIVE_STREAMING_ON_SOUND_FILE,
|
LIVE_STREAMING_ON_SOUND_FILE,
|
||||||
@@ -68,7 +76,7 @@ StateListenerRegistry.register(
|
|||||||
* @param {Store} store - The redux store.
|
* @param {Store} store - The redux store.
|
||||||
* @returns {Function}
|
* @returns {Function}
|
||||||
*/
|
*/
|
||||||
MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
MiddlewareRegistry.register(({ dispatch, getState }) => next => async action => {
|
||||||
let oldSessionData;
|
let oldSessionData;
|
||||||
|
|
||||||
if (action.type === RECORDING_SESSION_UPDATED) {
|
if (action.type === RECORDING_SESSION_UPDATED) {
|
||||||
@@ -123,6 +131,41 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case START_LOCAL_RECORDING: {
|
||||||
|
try {
|
||||||
|
await LocalRecordingManager.startLocalRecording({ dispatch,
|
||||||
|
getState });
|
||||||
|
const props = {
|
||||||
|
descriptionKey: 'recording.on',
|
||||||
|
titleKey: 'dialog.recording'
|
||||||
|
};
|
||||||
|
|
||||||
|
dispatch(playSound(RECORDING_ON_SOUND_ID));
|
||||||
|
dispatch(showNotification(props, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
|
||||||
|
dispatch(updateLocalRecordingStatus(true));
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('Capture failed', err);
|
||||||
|
|
||||||
|
const noTabError = err.message === 'WrongSurfaceSelected';
|
||||||
|
const props = {
|
||||||
|
descriptionKey: noTabError ? 'recording.surfaceError' : 'recording.error',
|
||||||
|
titleKey: 'recording.failedToStart'
|
||||||
|
};
|
||||||
|
|
||||||
|
dispatch(showErrorNotification(props, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case STOP_LOCAL_RECORDING: {
|
||||||
|
if (LocalRecordingManager.isRecordingLocally()) {
|
||||||
|
LocalRecordingManager.stopLocalRecording();
|
||||||
|
dispatch(playSound(RECORDING_OFF_SOUND_ID));
|
||||||
|
dispatch(updateLocalRecordingStatus(false));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case RECORDING_SESSION_UPDATED: {
|
case RECORDING_SESSION_UPDATED: {
|
||||||
// When in recorder mode no notifications are shown
|
// When in recorder mode no notifications are shown
|
||||||
// or extra sounds are also not desired
|
// or extra sounds are also not desired
|
||||||
@@ -211,6 +254,16 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case TRACK_ADDED: {
|
||||||
|
const { track } = action;
|
||||||
|
|
||||||
|
if (LocalRecordingManager.isRecordingLocally() && track.mediaType === MEDIA_TYPE.AUDIO) {
|
||||||
|
const audioTrack = track.jitsiTrack.track;
|
||||||
|
|
||||||
|
LocalRecordingManager.addAudioTrackToLocalRecording(audioTrack);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
Reference in New Issue
Block a user