Compare commits

...

11 Commits

Author SHA1 Message Date
Saúl Ibarra Corretgé 071531cf14 fix(android) make ongoing service public
Those using the view API may want to integrate iit in their own
Activity.
2022-07-01 12:12:07 +03:00
tmoldovan8x8 3185418df0 fix(android) explicitly sets the theme for JitsiMeetActivity 2022-06-30 17:46:39 +03:00
Saúl Ibarra Corretgé e66649b802 fix(rn,dynamic-branding) fix extracting fqdn from URL
On mobile we don't want to look in window.location.
2022-06-30 14:40:37 +03:00
Calin-Teodor a6fd5fd294 feat(dynamic-branding): get branding data from state 2022-06-30 09:07:19 +03:00
Alex Bumbu 41544f5314 fix(ios, pip): update view hierarchy to present the rn view with view controller 2022-06-29 18:58:24 +03:00
Saúl Ibarra Corretgé cfb944ee9b chore(,rn,versions) 22.3.1 2022-06-29 18:24:55 +03:00
Robert Pintilii a4394e3022 feat(recording) Add config to hide storage warning (#11761)
# Conflicts:
#	react/features/base/config/reducer.js
#	react/features/recording/components/Recording/AbstractStartRecordingDialog.js
2022-06-29 15:30:12 +03:00
Robert Pintilii aca7cc427c fix(local-recording) Improvements (#11754)
Show Start rec button if local rec is enabled but fileRecordings is disabled
Add warning for users to stop the recording
# Conflicts:
#	react/features/recording/components/Recording/AbstractStartRecordingDialog.js
2022-06-29 14:40:03 +03:00
Robert Pintilii b9973f65a2 feat(local-recording) Add self local recording (#11706)
Only record local participant audio/ video streams
# Conflicts:
#	react/features/recording/components/Recording/StartRecordingDialogContent.js
2022-06-29 14:39:14 +03:00
Calin Chitu 39437f6ac6 feat(gifs/native): fixed gify search input 2022-06-28 18:26:31 +03:00
tmoldovan8x8 8b6a1e4451 fix(rn, pip) enables PiP on conference mounted 2022-06-23 16:40:55 +02:00
46 changed files with 849 additions and 353 deletions

View File

@ -26,5 +26,5 @@ android.useAndroidX=true
android.enableJetifier=true
android.bundle.enableUncompressedNativeLibs=false
appVersion=22.3.0
appVersion=22.3.1
sdkVersion=5.2.0

View File

@ -32,6 +32,7 @@
android:name=".JitsiMeetActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
android:launchMode="singleTask"
android:theme="@style/JitsiMeetActivityStyle"
android:resizeableActivity="true"
android:supportsPictureInPicture="true"
android:windowSoftInputMode="adjustResize"/>

View File

@ -51,7 +51,7 @@ public class JitsiMeetOngoingConferenceService extends Service
private boolean isAudioMuted;
static void launch(Context context, HashMap<String, Object> extraData) {
public static void launch(Context context, HashMap<String, Object> extraData) {
OngoingNotification.createOngoingConferenceNotificationChannel();
Intent intent = new Intent(context, JitsiMeetOngoingConferenceService.class);
@ -80,7 +80,7 @@ public class JitsiMeetOngoingConferenceService extends Service
}
}
static void abort(Context context) {
public static void abort(Context context) {
Intent intent = new Intent(context, JitsiMeetOngoingConferenceService.class);
context.stopService(intent);
}

View File

@ -0,0 +1,3 @@
<resources>
<style name="JitsiMeetActivityStyle" parent="Theme.AppCompat.Light.NoActionBar"/>
</resources>

View File

@ -271,8 +271,9 @@ var config = {
// Recording
// Whether to enable file recording or not.
// DEPRECATED. Use recordingService.enabled instead.
// fileRecordingsEnabled: false,
// Enable the dropbox integration.
// dropbox: {
// appKey: '<APP_KEY>' // Specify your app key here.
@ -282,14 +283,27 @@ var config = {
// redirectURI:
// 'https://jitsi-meet.example.com/subfolder/static/oauth.html'
// },
// When integrations like dropbox are enabled only that will be shown,
// by enabling fileRecordingsServiceEnabled, we show both the integrations
// and the generic recording service (its configuration and storage type
// depends on jibri configuration)
// recordingService: {
// // When integrations like dropbox are enabled only that will be shown,
// // by enabling fileRecordingsServiceEnabled, we show both the integrations
// // and the generic recording service (its configuration and storage type
// // depends on jibri configuration)
// enabled: false,
// // Whether to show the possibility to share file recording with other people
// // (e.g. meeting participants), based on the actual implementation
// // on the backend.
// sharingEnabled: false,
// // Hide the warning that says we only store the recording for 24 hours.
// hideStorageWarning: false
// },
// DEPRECATED. Use recordingService.enabled instead.
// fileRecordingsServiceEnabled: false,
// Whether to show the possibility to share file recording with other people
// (e.g. meeting participants), based on the actual implementation
// on the backend.
// DEPRECATED. Use recordingService.sharingEnabled instead.
// fileRecordingsServiceSharingEnabled: false,
// Whether to enable live streaming or not.

View File

@ -19,6 +19,10 @@
font-size: 14px;
margin-left: 16px;
}
&.space-top {
margin-top: 10px;
}
}
.recording-header-line {

View File

@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>22.3.0</string>
<string>22.3.1</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>NSExtension</key>

View File

@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>22.3.0</string>
<string>22.3.1</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>

View File

@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>22.3.0</string>
<string>22.3.1</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>UISupportedInterfaceOrientations</key>

View File

@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>22.3.0</string>
<string>22.3.1</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>CLKComplicationPrincipalClass</key>

View File

@ -24,8 +24,14 @@
0BD906EA1EC0C00300C8C18E /* JitsiMeet.h in Headers */ = {isa = PBXBuildFile; fileRef = 0BD906E81EC0C00300C8C18E /* JitsiMeet.h */; settings = {ATTRIBUTES = (Public, ); }; };
4E51B76425E5345E0038575A /* ScheenshareEventEmiter.h in Headers */ = {isa = PBXBuildFile; fileRef = 4E51B76225E5345E0038575A /* ScheenshareEventEmiter.h */; };
4E51B76525E5345E0038575A /* ScheenshareEventEmiter.m in Sources */ = {isa = PBXBuildFile; fileRef = 4E51B76325E5345E0038575A /* ScheenshareEventEmiter.m */; };
4EBA6E61286072E300B31882 /* JitsiMeetViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 4EBA6E5F286072E300B31882 /* JitsiMeetViewController.h */; };
4EBA6E62286072E300B31882 /* JitsiMeetViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4EBA6E60286072E300B31882 /* JitsiMeetViewController.m */; };
4EBA6E652860B1E800B31882 /* JitsiMeetRenderingView.h in Headers */ = {isa = PBXBuildFile; fileRef = 4EBA6E632860B1E800B31882 /* JitsiMeetRenderingView.h */; };
4EBA6E662860B1E800B31882 /* JitsiMeetRenderingView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4EBA6E642860B1E800B31882 /* JitsiMeetRenderingView.m */; };
4ED4FFF32721B9B90074E620 /* JitsiAudioSession.h in Headers */ = {isa = PBXBuildFile; fileRef = 4ED4FFF12721B9B90074E620 /* JitsiAudioSession.h */; settings = {ATTRIBUTES = (Public, ); }; };
4ED4FFF42721B9B90074E620 /* JitsiAudioSession.m in Sources */ = {isa = PBXBuildFile; fileRef = 4ED4FFF22721B9B90074E620 /* JitsiAudioSession.m */; };
4EEC9630286C73A2008705FA /* JitsiMeetView+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 4EEC962E286C73A2008705FA /* JitsiMeetView+Private.h */; };
4EEC9631286C73A2008705FA /* JitsiMeetView+Private.m in Sources */ = {isa = PBXBuildFile; fileRef = 4EEC962F286C73A2008705FA /* JitsiMeetView+Private.m */; };
6F08DF7D4458EE3CF3F36F6D /* libPods-JitsiMeetSDK.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E4376CA6886DE68FD7A4294B /* libPods-JitsiMeetSDK.a */; };
A4A934E9212F3ADB001E9388 /* Dropbox.m in Sources */ = {isa = PBXBuildFile; fileRef = A4A934E8212F3ADB001E9388 /* Dropbox.m */; };
C6245F5D2053091D0040BE68 /* image-resize@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = C6245F5B2053091D0040BE68 /* image-resize@2x.png */; };
@ -79,9 +85,15 @@
0BD906E91EC0C00300C8C18E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
4E51B76225E5345E0038575A /* ScheenshareEventEmiter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ScheenshareEventEmiter.h; sourceTree = "<group>"; };
4E51B76325E5345E0038575A /* ScheenshareEventEmiter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ScheenshareEventEmiter.m; sourceTree = "<group>"; };
4EBA6E5F286072E300B31882 /* JitsiMeetViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JitsiMeetViewController.h; sourceTree = "<group>"; };
4EBA6E60286072E300B31882 /* JitsiMeetViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = JitsiMeetViewController.m; sourceTree = "<group>"; };
4EBA6E632860B1E800B31882 /* JitsiMeetRenderingView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JitsiMeetRenderingView.h; sourceTree = "<group>"; };
4EBA6E642860B1E800B31882 /* JitsiMeetRenderingView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = JitsiMeetRenderingView.m; sourceTree = "<group>"; };
4ED4FFF12721B9B90074E620 /* JitsiAudioSession.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JitsiAudioSession.h; sourceTree = "<group>"; };
4ED4FFF22721B9B90074E620 /* JitsiAudioSession.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = JitsiAudioSession.m; sourceTree = "<group>"; };
4ED4FFF52721BAE10074E620 /* JitsiAudioSession+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "JitsiAudioSession+Private.h"; sourceTree = "<group>"; };
4EEC962E286C73A2008705FA /* JitsiMeetView+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "JitsiMeetView+Private.h"; sourceTree = "<group>"; };
4EEC962F286C73A2008705FA /* JitsiMeetView+Private.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "JitsiMeetView+Private.m"; sourceTree = "<group>"; };
891FE43DAD30BC8976683100 /* Pods-JitsiMeetSDK.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JitsiMeetSDK.release.xcconfig"; path = "../Pods/Target Support Files/Pods-JitsiMeetSDK/Pods-JitsiMeetSDK.release.xcconfig"; sourceTree = "<group>"; };
98E09B5C73D9036B4ED252FC /* Pods-JitsiMeet.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JitsiMeet.debug.xcconfig"; path = "../Pods/Target Support Files/Pods-JitsiMeet/Pods-JitsiMeet.debug.xcconfig"; sourceTree = "<group>"; };
9C77CA3CC919B081F1A52982 /* Pods-JitsiMeet.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JitsiMeet.release.xcconfig"; path = "../Pods/Target Support Files/Pods-JitsiMeet/Pods-JitsiMeet.release.xcconfig"; sourceTree = "<group>"; };
@ -94,7 +106,6 @@
C69EFA0B209A0F660027712B /* JMCallKitListener.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JMCallKitListener.swift; sourceTree = "<group>"; };
C6A3425E204EF76800E062DD /* DragGestureController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DragGestureController.swift; sourceTree = "<group>"; };
C6CC49AE207412CF000DFA42 /* PiPViewCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PiPViewCoordinator.swift; sourceTree = "<group>"; };
C6F99C13204DB63D0001F710 /* JitsiMeetView+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "JitsiMeetView+Private.h"; sourceTree = "<group>"; };
C81E9AB825AC5AD800B134D9 /* ExternalAPI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ExternalAPI.h; sourceTree = "<group>"; };
C8AFD27D2462C613000293D2 /* InfoPlistUtil.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = InfoPlistUtil.h; sourceTree = "<group>"; };
C8AFD27E2462C613000293D2 /* InfoPlistUtil.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = InfoPlistUtil.m; sourceTree = "<group>"; };
@ -194,12 +205,17 @@
DE65AACB2318028300290BEC /* JitsiMeetBaseLogHandler+Private.h */,
DE81A2DD2317ED5400AE1940 /* JitsiMeetBaseLogHandler.m */,
0B412F161EDEC65D00B1A0A6 /* JitsiMeetView.h */,
4EEC962E286C73A2008705FA /* JitsiMeetView+Private.h */,
4EEC962F286C73A2008705FA /* JitsiMeetView+Private.m */,
0B412F171EDEC65D00B1A0A6 /* JitsiMeetView.m */,
4EBA6E632860B1E800B31882 /* JitsiMeetRenderingView.h */,
4EBA6E642860B1E800B31882 /* JitsiMeetRenderingView.m */,
4EBA6E5F286072E300B31882 /* JitsiMeetViewController.h */,
4EBA6E60286072E300B31882 /* JitsiMeetViewController.m */,
DE81A2D72316AC7600AE1940 /* LogBridge.m */,
DE65AAC92317FFCD00290BEC /* LogUtils.h */,
DEAFA777229EAD3B0033A7FA /* RNRootView.h */,
DEAFA778229EAD520033A7FA /* RNRootView.m */,
C6F99C13204DB63D0001F710 /* JitsiMeetView+Private.h */,
0B412F1B1EDEC80100B1A0A6 /* JitsiMeetViewDelegate.h */,
DEFC743D21B178FA00E4DD96 /* LocaleDetector.m */,
C6A3426B204F127900E062DD /* picture-in-picture */,
@ -284,11 +300,14 @@
DEA9F284258A5D9900D4CD74 /* JitsiMeetSDK.h in Headers */,
4E51B76425E5345E0038575A /* ScheenshareEventEmiter.h in Headers */,
DE65AACC2318028300290BEC /* JitsiMeetBaseLogHandler+Private.h in Headers */,
4EBA6E652860B1E800B31882 /* JitsiMeetRenderingView.h in Headers */,
0B412F221EDEF6EA00B1A0A6 /* JitsiMeetViewDelegate.h in Headers */,
4ED4FFF32721B9B90074E620 /* JitsiAudioSession.h in Headers */,
4EEC9630286C73A2008705FA /* JitsiMeetView+Private.h in Headers */,
0BD906EA1EC0C00300C8C18E /* JitsiMeet.h in Headers */,
DE81A2D42316AC4D00AE1940 /* JitsiMeetLogger.h in Headers */,
DE65AACA2317FFCD00290BEC /* LogUtils.h in Headers */,
4EBA6E61286072E300B31882 /* JitsiMeetViewController.h in Headers */,
DEAD3226220C497000E93636 /* JitsiMeetConferenceOptions.h in Headers */,
C81E9AB925AC5AD800B134D9 /* ExternalAPI.h in Headers */,
C8AFD27F2462C613000293D2 /* InfoPlistUtil.h in Headers */,
@ -449,6 +468,7 @@
files = (
0BB9AD7B1F5EC8F4001C08DB /* CallKit.m in Sources */,
DE81A2DF2317ED5400AE1940 /* JitsiMeetBaseLogHandler.m in Sources */,
4EBA6E662860B1E800B31882 /* JitsiMeetRenderingView.m in Sources */,
4ED4FFF42721B9B90074E620 /* JitsiAudioSession.m in Sources */,
0BB9AD7D1F60356D001C08DB /* AppInfo.m in Sources */,
DE81A2D92316AC7600AE1940 /* LogBridge.m in Sources */,
@ -465,9 +485,11 @@
0BCA49611EC4B6C600B793EE /* Proximity.m in Sources */,
C69EFA0C209A0F660027712B /* JMCallKitEmitter.swift in Sources */,
DEFE535621FB2E8300011A3A /* ReactUtils.m in Sources */,
4EEC9631286C73A2008705FA /* JitsiMeetView+Private.m in Sources */,
C6A34261204EF76800E062DD /* DragGestureController.swift in Sources */,
4E51B76525E5345E0038575A /* ScheenshareEventEmiter.m in Sources */,
A4A934E9212F3ADB001E9388 /* Dropbox.m in Sources */,
4EBA6E62286072E300B31882 /* JitsiMeetViewController.m in Sources */,
C69EFA0D209A0F660027712B /* JMCallKitProxy.swift in Sources */,
DE81A2D52316AC4D00AE1940 /* JitsiMeetLogger.m in Sources */,
C69EFA0E209A0F660027712B /* JMCallKitListener.swift in Sources */,

View File

@ -16,6 +16,8 @@
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>
static NSString * const sendEventNotificationName = @"org.jitsi.meet.SendEvent";
@interface ExternalAPI : RCTEventEmitter<RCTBridgeModule>
- (void)sendHangUp;

View File

@ -15,7 +15,6 @@
*/
#import "ExternalAPI.h"
#import "JitsiMeetView+Private.h"
// Events
static NSString * const hangUpAction = @"org.jitsi.meet.HANG_UP";
@ -91,31 +90,14 @@ RCT_EXPORT_MODULE();
RCT_EXPORT_METHOD(sendEvent:(NSString *)name
data:(NSDictionary *)data
scope:(NSString *)scope) {
// The JavaScript App needs to provide uniquely identifying information to
// the native ExternalAPI module so that the latter may match the former
// to the native JitsiMeetView which hosts it.
JitsiMeetView *view = [JitsiMeetView viewForExternalAPIScope:scope];
if (!view) {
return;
}
id delegate = view.delegate;
if (!delegate) {
return;
}
if ([name isEqual: @"PARTICIPANTS_INFO_RETRIEVED"]) {
[self onParticipantsInfoRetrieved: data];
return;
}
SEL sel = NSSelectorFromString([self methodNameFromEventName:name]);
if (sel && [delegate respondsToSelector:sel]) {
[delegate performSelector:sel withObject:data];
}
[[NSNotificationCenter defaultCenter] postNotificationName:sendEventNotificationName
object:nil
userInfo:@{@"name": name, @"data": data}];
}
- (void) onParticipantsInfoRetrieved:(NSDictionary *)data {
@ -127,28 +109,6 @@ RCT_EXPORT_METHOD(sendEvent:(NSString *)name
[participantInfoCompletionHandlers removeObjectForKey:completionHandlerId];
}
/**
* Converts a specific event name i.e. redux action type description to a
* method name.
*
* @param eventName The event name to convert to a method name.
* @return A method name constructed from the specified `eventName`.
*/
- (NSString *)methodNameFromEventName:(NSString *)eventName {
NSMutableString *methodName
= [NSMutableString stringWithCapacity:eventName.length];
for (NSString *c in [eventName componentsSeparatedByString:@"_"]) {
if (c.length) {
[methodName appendString:
methodName.length ? c.capitalizedString : c.lowercaseString];
}
}
[methodName appendString:@":"];
return methodName;
}
- (void)sendHangUp {
[self sendEventWithName:hangUpAction body:nil];
}

View File

@ -15,6 +15,8 @@
*/
#import <Intents/Intents.h>
#import <RNGoogleSignin/RNGoogleSignin.h>
#import <WebRTC/RTCLogging.h>
#import "Dropbox.h"
#import "JitsiMeet+Private.h"
@ -25,9 +27,6 @@
#import "RNSplashScreen.h"
#import "ScheenshareEventEmiter.h"
#import <RNGoogleSignin/RNGoogleSignin.h>
#import <WebRTC/RTCLogging.h>
@implementation JitsiMeet {
RCTBridgeWrapper *_bridgeWrapper;
NSDictionary *_launchOptions;
@ -87,8 +86,12 @@
restorationHandler:(void (^)(NSArray<id<UIUserActivityRestoring>> *))restorationHandler {
JitsiMeetConferenceOptions *options = [self optionsFromUserActivity:userActivity];
if (options) {
[JitsiMeetView updateProps:[options asProps]];
return true;
}
return options && [JitsiMeetView setPropsInViews:[options asProps]];
return false;
}
- (BOOL)application:(UIApplication *)app
@ -112,8 +115,9 @@
JitsiMeetConferenceOptions *conferenceOptions = [JitsiMeetConferenceOptions fromBuilder:^(JitsiMeetConferenceOptionsBuilder *builder) {
builder.room = [url absoluteString];
}];
[JitsiMeetView updateProps:[conferenceOptions asProps]];
return [JitsiMeetView setPropsInViews:[conferenceOptions asProps]];
return true;
}
#pragma mark - Utility methods

View File

@ -0,0 +1,30 @@
/*
* Copyright @ 2022-present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <UIKit/UIKit.h>
#import "JitsiMeetViewDelegate.h"
NS_ASSUME_NONNULL_BEGIN
@interface JitsiMeetRenderingView : UIView
@property (nonatomic, assign) BOOL isPiPEnabled;
- (void)setProps:(NSDictionary *_Nonnull)newProps;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,103 @@
/*
* Copyright @ 2022-present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <mach/mach_time.h>
#import "JitsiMeetRenderingView.h"
#import "ReactUtils.h"
#import "RNRootView.h"
#import "JitsiMeet+Private.h"
/**
* Backwards compatibility: turn the boolean prop into a feature flag.
*/
static NSString *const PiPEnabledFeatureFlag = @"pip.enabled";
@interface JitsiMeetRenderingView ()
/**
* The unique identifier of this `JitsiMeetView` within the process for the
* purposes of `ExternalAPI`. The name scope was inspired by postis which we
* use on Web for the similar purposes of the iframe-based external API.
*/
@property (nonatomic, strong) NSString *externalAPIScope;
@end
@implementation JitsiMeetRenderingView {
/**
* React Native view where the entire content will be rendered.
*/
RNRootView *rootView;
}
- (instancetype)init {
self = [super init];
if (self) {
// Hook this JitsiMeetView into ExternalAPI.
self.externalAPIScope = [NSUUID UUID].UUIDString;
}
return self;
}
/**
* Passes the given props to the React Native application. The props which we pass
* are a combination of 3 different sources:
*
* - JitsiMeet.defaultConferenceOptions
* - This function's parameters
* - Some extras which are added by this function
*/
- (void)setProps:(NSDictionary *_Nonnull)newProps {
NSMutableDictionary *props = mergeProps([[JitsiMeet sharedInstance] getDefaultProps], newProps);
// Set the PiP flag if it wasn't manually set.
NSMutableDictionary *featureFlags = props[@"flags"];
// TODO: temporary implementation
if (featureFlags[PiPEnabledFeatureFlag] == nil) {
featureFlags[PiPEnabledFeatureFlag] = @(self.isPiPEnabled);
}
props[@"externalAPIScope"] = self.externalAPIScope;
// This method is supposed to be imperative i.e. a second
// invocation with one and the same URL is expected to join the respective
// conference again if the first invocation was followed by leaving the
// conference. However, React and, respectively,
// appProperties/initialProperties are declarative expressions i.e. one and
// the same URL will not trigger an automatic re-render in the JavaScript
// source code. The workaround implemented below introduces imperativeness
// in React Component props by defining a unique value per invocation.
props[@"timestamp"] = @(mach_absolute_time());
if (rootView) {
// Update props with the new URL.
rootView.appProperties = props;
} else {
RCTBridge *bridge = [[JitsiMeet sharedInstance] getReactBridge];
rootView = [[RNRootView alloc] initWithBridge:bridge
moduleName:@"App"
initialProperties:props];
rootView.backgroundColor = self.backgroundColor;
// Add rootView as a subview which completely covers this one.
[rootView setFrame:[self bounds]];
rootView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
[self addSubview:rootView];
}
}
@end

View File

@ -1,6 +1,5 @@
/*
* Copyright @ 2018-present 8x8, Inc.
* Copyright @ 2017-2018 Atlassian Pty Ltd
* Copyright @ 2022-present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -15,11 +14,16 @@
* limitations under the License.
*/
#import "JitsiMeetView.h"
#import <JitsiMeetSDK/JitsiMeetSDK.h>
@interface JitsiMeetView ()
NS_ASSUME_NONNULL_BEGIN
+ (instancetype _Nullable)viewForExternalAPIScope:(NSString *_Nonnull)externalAPIScope;
+ (BOOL)setPropsInViews:(NSDictionary *_Nonnull)newProps;
static NSString * const updateViewPropsNotificationName = @"org.jitsi.meet.UpdateViewProps";
@interface JitsiMeetView (Private)
+ (void)updateProps:(NSDictionary *_Nonnull)newProps;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,25 @@
/*
* Copyright @ 2022-present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "JitsiMeetView+Private.h"
@implementation JitsiMeetView (Private)
+ (void)updateProps:(NSDictionary *_Nonnull)newProps {
[[NSNotificationCenter defaultCenter] postNotificationName:updateViewPropsNotificationName object:nil userInfo:@{@"props": newProps}];
}
@end

View File

@ -20,43 +20,22 @@
#import "ExternalAPI.h"
#import "JitsiMeet+Private.h"
#import "JitsiMeetConferenceOptions+Private.h"
#import "JitsiMeetView+Private.h"
#import "JitsiMeetView.h"
#import "JitsiMeetViewController.h"
#import "ReactUtils.h"
#import "RNRootView.h"
@interface JitsiMeetView ()
/**
* Backwards compatibility: turn the boolean prop into a feature flag.
*/
static NSString *const PiPEnabledFeatureFlag = @"pip.enabled";
@property (nonatomic, strong) JitsiMeetViewController *jitsiMeetViewController;
@property (nonatomic, strong) UINavigationController *navController;
@property (nonatomic, readonly) BOOL isPiPEnabled;
@end
@implementation JitsiMeetView {
/**
* The unique identifier of this `JitsiMeetView` within the process for the
* purposes of `ExternalAPI`. The name scope was inspired by postis which we
* use on Web for the similar purposes of the iframe-based external API.
*/
NSString *externalAPIScope;
@implementation JitsiMeetView
/**
* React Native view where the entire content will be rendered.
*/
RNRootView *rootView;
}
/**
* The `JitsiMeetView`s associated with their `ExternalAPI` scopes (i.e. unique
* identifiers within the process).
*/
static NSMapTable<NSString *, JitsiMeetView *> *views;
/**
* This gets called automagically when the program starts.
*/
__attribute__((constructor))
static void initializeViewsMap() {
views = [NSMapTable strongToWeakObjectsMapTable];
}
@dynamic isPiPEnabled;
#pragma mark Initializers
@ -87,6 +66,10 @@ static void initializeViewsMap() {
return self;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
/**
* Internal initialization:
*
@ -94,70 +77,57 @@ static void initializeViewsMap() {
* - initializes the external API scope
*/
- (void)initWithXXX {
// Hook this JitsiMeetView into ExternalAPI.
externalAPIScope = [NSUUID UUID].UUIDString;
[views setObject:self forKey:externalAPIScope];
// Set a background color which is in accord with the JavaScript and Android
// parts of the application and causes less perceived visual flicker than
// the default background color.
self.backgroundColor
= [UIColor colorWithRed:.07f green:.07f blue:.07f alpha:1];
self.jitsiMeetViewController = [[JitsiMeetViewController alloc] init];
self.jitsiMeetViewController.view.frame = [self bounds];
[self addSubview:self.jitsiMeetViewController.view];
[self registerObservers];
}
#pragma mark API
- (void)join:(JitsiMeetConferenceOptions *)options {
[self setProps:options == nil ? @{} : [options asProps]];
[self.jitsiMeetViewController join:options withPiP:self.isPiPEnabled];
}
- (void)leave {
[self setProps:@{}];
[self.jitsiMeetViewController leave];
}
- (void)hangUp {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI sendHangUp];
[self.jitsiMeetViewController hangUp];
}
- (void)setAudioMuted:(BOOL)muted {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI sendSetAudioMuted:muted];
[self.jitsiMeetViewController setAudioMuted:muted];
}
- (void)sendEndpointTextMessage:(NSString * _Nonnull)message :(NSString * _Nullable)to {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI sendEndpointTextMessage:message :to];
[self.jitsiMeetViewController sendEndpointTextMessage:message :to];
}
- (void)toggleScreenShare:(BOOL)enabled {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI toggleScreenShare:enabled];
[self.jitsiMeetViewController toggleScreenShare:enabled];
}
- (void)retrieveParticipantsInfo:(void (^ _Nonnull)(NSArray * _Nullable))completionHandler {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI retrieveParticipantsInfo:completionHandler];
[self.jitsiMeetViewController retrieveParticipantsInfo:completionHandler];
}
- (void)openChat:(NSString*)to {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI openChat:to];
[self.jitsiMeetViewController openChat:to];
}
- (void)closeChat {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI closeChat];
[self.jitsiMeetViewController closeChat];
}
- (void)sendChatMessage:(NSString * _Nonnull)message :(NSString * _Nullable)to {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI sendChatMessage:message :to];
[self.jitsiMeetViewController sendChatMessage:message :to];
}
- (void)setVideoMuted:(BOOL)muted {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI sendSetVideoMuted:muted];
[self.jitsiMeetViewController setVideoMuted:muted];
}
- (void)setClosedCaptionsEnabled:(BOOL)enabled {
@ -165,79 +135,47 @@ static void initializeViewsMap() {
[externalAPI sendSetClosedCaptionsEnabled:enabled];
}
#pragma mark Private methods
#pragma mark Private
- (BOOL)isPiPEnabled {
return self.delegate && [self.delegate respondsToSelector:@selector(enterPictureInPicture:)];
}
- (void)registerObservers {
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleSendEventNotification:) name:sendEventNotificationName object:nil];
}
- (void)handleSendEventNotification:(NSNotification *)notification {
NSString *eventName = notification.userInfo[@"name"];
NSString *eventData = notification.userInfo[@"data"];
SEL sel = NSSelectorFromString([self methodNameFromEventName:eventName]);
if (sel && [self.delegate respondsToSelector:sel]) {
[self.delegate performSelector:sel withObject:eventData];
}
}
/**
* Passes the given props to the React Native application. The props which we pass
* are a combination of 3 different sources:
* Converts a specific event name i.e. redux action type description to a
* method name.
*
* - JitsiMeet.defaultConferenceOptions
* - This function's parameters
* - Some extras which are added by this function
* @param eventName The event name to convert to a method name.
* @return A method name constructed from the specified `eventName`.
*/
- (void)setProps:(NSDictionary *_Nonnull)newProps {
NSMutableDictionary *props = mergeProps([[JitsiMeet sharedInstance] getDefaultProps], newProps);
- (NSString *)methodNameFromEventName:(NSString *)eventName {
NSMutableString *methodName
= [NSMutableString stringWithCapacity:eventName.length];
// Set the PiP flag if it wasn't manually set.
NSMutableDictionary *featureFlags = props[@"flags"];
if (featureFlags[PiPEnabledFeatureFlag] == nil) {
featureFlags[PiPEnabledFeatureFlag]
= [NSNumber numberWithBool:
self.delegate && [self.delegate respondsToSelector:@selector(enterPictureInPicture:)]];
}
for (NSString *c in [eventName componentsSeparatedByString:@"_"]) {
if (c.length) {
[methodName appendString:
methodName.length ? c.capitalizedString : c.lowercaseString];
}
}
[methodName appendString:@":"];
props[@"externalAPIScope"] = externalAPIScope;
// This method is supposed to be imperative i.e. a second
// invocation with one and the same URL is expected to join the respective
// conference again if the first invocation was followed by leaving the
// conference. However, React and, respectively,
// appProperties/initialProperties are declarative expressions i.e. one and
// the same URL will not trigger an automatic re-render in the JavaScript
// source code. The workaround implemented below introduces imperativeness
// in React Component props by defining a unique value per invocation.
props[@"timestamp"] = @(mach_absolute_time());
if (rootView) {
// Update props with the new URL.
rootView.appProperties = props;
} else {
RCTBridge *bridge = [[JitsiMeet sharedInstance] getReactBridge];
rootView
= [[RNRootView alloc] initWithBridge:bridge
moduleName:@"App"
initialProperties:props];
rootView.backgroundColor = self.backgroundColor;
// Add rootView as a subview which completely covers this one.
[rootView setFrame:[self bounds]];
rootView.autoresizingMask
= UIViewAutoresizingFlexibleWidth
| UIViewAutoresizingFlexibleHeight;
[self addSubview:rootView];
}
}
+ (BOOL)setPropsInViews:(NSDictionary *_Nonnull)newProps {
BOOL handled = NO;
if (views) {
for (NSString *externalAPIScope in views) {
JitsiMeetView *view
= [self viewForExternalAPIScope:externalAPIScope];
if (view) {
[view setProps:newProps];
handled = YES;
}
}
}
return handled;
}
+ (instancetype)viewForExternalAPIScope:(NSString *)externalAPIScope {
return [views objectForKey:externalAPIScope];
return methodName;
}
@end

View File

@ -0,0 +1,38 @@
/*
* Copyright @ 2022-present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <UIKit/UIKit.h>
#import "JitsiMeetConferenceOptions.h"
NS_ASSUME_NONNULL_BEGIN
@interface JitsiMeetViewController : UIViewController
- (void)join:(JitsiMeetConferenceOptions *)options withPiP:(BOOL)enablePiP;
- (void)leave;
- (void)hangUp;
- (void)setAudioMuted:(BOOL)muted;
- (void)sendEndpointTextMessage:(NSString * _Nonnull)message :(NSString * _Nullable)to;
- (void)toggleScreenShare:(BOOL)enabled;
- (void)retrieveParticipantsInfo:(void (^ _Nonnull)(NSArray * _Nullable))completionHandler;
- (void)openChat:(NSString*)to;
- (void)closeChat;
- (void)sendChatMessage:(NSString * _Nonnull)message :(NSString * _Nullable)to;
- (void)setVideoMuted:(BOOL)muted;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,127 @@
/*
* Copyright @ 2022-present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "JitsiMeetViewController.h"
#import "JitsiMeet+Private.h"
#import "JitsiMeetConferenceOptions+Private.h"
#import "JitsiMeetRenderingView.h"
#import "JitsiMeetView+Private.h"
@interface JitsiMeetViewController ()
@property (strong, nonatomic) JitsiMeetRenderingView *view;
@end
@implementation JitsiMeetViewController
@dynamic view;
- (instancetype)init {
self = [super init];
if (self) {
[self registerObservers];
}
return self;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)loadView {
[super loadView];
self.view = [[JitsiMeetRenderingView alloc] init];
self.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
}
- (void)viewDidLoad {
[super viewDidLoad];
// Set a background color which is in accord with the JavaScript and Android
// parts of the application and causes less perceived visual flicker than
// the default background color.
self.view.backgroundColor = [UIColor colorWithRed:.07f green:.07f blue:.07f alpha:1];
}
- (void)join:(JitsiMeetConferenceOptions *)options withPiP:(BOOL)enablePiP {
self.view.isPiPEnabled = enablePiP;
[self.view setProps:options == nil ? @{} : [options asProps]];
}
- (void)leave {
[self.view setProps:@{}];
}
- (void)hangUp {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI sendHangUp];
}
- (void)setAudioMuted:(BOOL)muted {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI sendSetAudioMuted:muted];
}
- (void)sendEndpointTextMessage:(NSString * _Nonnull)message :(NSString * _Nullable)to {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI sendEndpointTextMessage:message :to];
}
- (void)toggleScreenShare:(BOOL)enabled {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI toggleScreenShare:enabled];
}
- (void)retrieveParticipantsInfo:(void (^ _Nonnull)(NSArray * _Nullable))completionHandler {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI retrieveParticipantsInfo:completionHandler];
}
- (void)openChat:(NSString*)to {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI openChat:to];
}
- (void)closeChat {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI closeChat];
}
- (void)sendChatMessage:(NSString * _Nonnull)message :(NSString * _Nullable)to {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI sendChatMessage:message :to];
}
- (void)setVideoMuted:(BOOL)muted {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI sendSetVideoMuted:muted];
}
#pragma mark Private
- (void)registerObservers {
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleUpdateViewPropsNotification:) name:updateViewPropsNotificationName object:nil];
}
- (void)handleUpdateViewPropsNotification:(NSNotification *)notification {
NSDictionary *props = [notification.userInfo objectForKey:@"props"];
[self.view setProps:props];
}
@end

View File

@ -895,12 +895,19 @@
"linkGenerated": "We have generated a link to your recording.",
"live": "LIVE",
"localRecordingNoNotificationWarning": "The recording will not be announced to other participants. You will need to let them know that the meeting is recorded.",
"localRecordingNoVideo": "Video is not being recorded",
"localRecordingStartWarning": "Please make sure you stop the recording before exiting the meeting in order to save it.",
"localRecordingStartWarningTitle": "Stop the recording to save it",
"localRecordingVideoStop": "Stopping your video will also stop the local recording. Are you sure you want to continue?",
"localRecordingVideoWarning": "To record your video you must have it on when starting the recording",
"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}}",
"noStreams": "No audio or video stream detected.",
"off": "Recording stopped",
"offBy": "{{name}} stopped the recording",
"on": "Recording started",
"onBy": "{{name}} started the recording",
"onlyRecordSelf": "Record only my audio and video streams",
"pending": "Preparing to record the meeting...",
"rec": "REC",
"saveLocalRecording": "Save recording file locally",

View File

@ -3,6 +3,7 @@
import '../authentication/middleware';
import '../base/i18n/middleware';
import '../base/devices/middleware';
import '../base/media/middleware';
import '../dynamic-branding/middleware';
import '../e2ee/middleware';
import '../external-api/middleware';

View File

@ -353,6 +353,20 @@ function _translateLegacyConfig(oldValue: Object) {
newValue.defaultRemoteDisplayName
= newValue.defaultRemoteDisplayName || 'Fellow Jitster';
newValue.recordingService = newValue.recordingService || {};
if (oldValue.fileRecordingsServiceEnabled !== undefined) {
newValue.recordingService = {
...newValue.recordingService,
enabled: oldValue.fileRecordingsServiceEnabled
};
}
if (oldValue.fileRecordingsServiceSharingEnabled !== undefined) {
newValue.recordingService = {
...newValue.recordingService,
sharingEnabled: oldValue.fileRecordingsServiceSharingEnabled
};
}
return newValue;
}

View File

@ -0,0 +1 @@
import './middleware.any.js';

View File

@ -0,0 +1,40 @@
import './middleware.any.js';
// @ts-ignore
import { MiddlewareRegistry } from '../redux';
import { IStore } from '../../app/types';
import { SET_VIDEO_MUTED } from './actionTypes';
import LocalRecordingManager from '../../recording/components/Recording/LocalRecordingManager.web';
// @ts-ignore
import { openDialog } from '../dialog';
// @ts-ignore
import { NOTIFICATION_TIMEOUT_TYPE, showNotification } from '../../notifications';
// @ts-ignore
import StopRecordingDialog from '../../recording/components/Recording/web/StopRecordingDialog';
/**
* Implements the entry point of the middleware of the feature base/media.
*
* @param {IStore} store - The redux store.
* @returns {Function}
*/
MiddlewareRegistry.register((store: IStore) => (next: Function) => (action: any) => {
const { dispatch } = store;
switch(action.type) {
case SET_VIDEO_MUTED: {
if (LocalRecordingManager.isRecordingLocally() && LocalRecordingManager.selfRecording.on) {
if (action.muted && LocalRecordingManager.selfRecording.withVideo) {
dispatch(openDialog(StopRecordingDialog, { localRecordingVideoStop: true }));
return;
} else if (!action.muted && !LocalRecordingManager.selfRecording.withVideo) {
dispatch(showNotification({
titleKey: 'recording.localRecordingNoVideo',
descriptionKey: 'recording.localRecordingVideoWarning',
uid: 'recording.localRecordingNoVideo'
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
}
}
}
}
return next(action);
});

View File

@ -236,7 +236,8 @@ export const OVERWRITE_PARTICIPANTS_NAMES = 'OVERWRITE_PARTICIPANTS_NAMES';
* Updates participants local recording status.
* {
* type: SET_LOCAL_PARTICIPANT_RECORDING_STATUS,
* recording: boolean
* recording: boolean,
* onlySelf: boolean
* }
*/
export const SET_LOCAL_PARTICIPANT_RECORDING_STATUS = 'SET_LOCAL_PARTICIPANT_RECORDING_STATUS';

View File

@ -689,14 +689,16 @@ export function overwriteParticipantsNames(participantList) {
* Local video recording status for the local participant.
*
* @param {boolean} recording - If local recording is ongoing.
* @param {boolean} onlySelf - If recording only local streams.
* @returns {{
* type: SET_LOCAL_PARTICIPANT_RECORDING_STATUS,
* recording: boolean
* }}
*/
export function updateLocalRecordingStatus(recording) {
export function updateLocalRecordingStatus(recording, onlySelf) {
return {
type: SET_LOCAL_PARTICIPANT_RECORDING_STATUS,
recording
recording,
onlySelf
};
}

View File

@ -179,11 +179,11 @@ MiddlewareRegistry.register(store => next => action => {
case SET_LOCAL_PARTICIPANT_RECORDING_STATUS: {
const state = store.getState();
const { recording } = action;
const { recording, onlySelf } = action;
const localId = getLocalParticipant(state)?.id;
const { localRecording } = state['features/base/config'];
if (localRecording.notifyAllParticipants) {
if (localRecording?.notifyAllParticipants && !onlySelf) {
store.dispatch(participantUpdated({
// XXX Only the local participant is allowed to update without
// stating the JitsiConference instance (i.e. participant property

View File

@ -29,6 +29,7 @@ import { navigate }
from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
import { shouldEnableAutoKnock } from '../../../mobile/navigation/functions';
import { screen } from '../../../mobile/navigation/routes';
import { setPictureInPictureEnabled } from '../../../mobile/picture-in-picture';
import { Captions } from '../../../subtitles';
import { setToolboxVisible } from '../../../toolbox/actions';
import { Toolbox } from '../../../toolbox/components/native';
@ -191,6 +192,7 @@ class Conference extends AbstractConference<Props, State> {
*/
componentDidMount() {
BackHandler.addEventListener('hardwareBackPress', this._onHardwareBackPress);
setPictureInPictureEnabled(true);
}
/**
@ -231,6 +233,7 @@ class Conference extends AbstractConference<Props, State> {
BackHandler.removeEventListener('hardwareBackPress', this._onHardwareBackPress);
clearTimeout(this._expandedLabelTimeout.current);
setPictureInPictureEnabled(false);
}
/**

View File

@ -22,7 +22,7 @@ export function fetchCustomBrandingData() {
const { customizationReady } = state['features/dynamic-branding'];
if (!customizationReady) {
const url = await getDynamicBrandingUrl();
const url = await getDynamicBrandingUrl(state);
if (url) {
try {

View File

@ -6,6 +6,7 @@ import {
setDynamicBrandingFailed,
setDynamicBrandingReady
} from './actions.any';
import { getDynamicBrandingUrl } from './functions.any';
import logger from './logger';
@ -19,7 +20,7 @@ import logger from './logger';
export function fetchCustomBrandingData() {
return async function(dispatch: Function, getState: Function) {
const state = getState();
const { dynamicBrandingUrl } = state['features/base/config'];
const dynamicBrandingUrl = await getDynamicBrandingUrl(state);
if (dynamicBrandingUrl) {
try {

View File

@ -1,6 +1,7 @@
// @flow
import { loadConfig } from '../base/lib-jitsi-meet/functions';
import { toState } from '../base/redux';
/**
* Extracts the fqn part from a path, where fqn represents
@ -29,10 +30,13 @@ export function extractFqnFromPath(state?: Object) {
/**
* Returns the url used for fetching dynamic branding.
*
* @param {Object | Function} stateful - The redux store, state, or
* {@code getState} function.
* @returns {string}
*/
export async function getDynamicBrandingUrl() {
const config = await loadConfig(window.location.href);
export async function getDynamicBrandingUrl(stateful: Object | Function) {
const state = toState(stateful);
const config = state['features/base/config'];
const { dynamicBrandingUrl } = config;
if (dynamicBrandingUrl) {
@ -40,7 +44,7 @@ export async function getDynamicBrandingUrl() {
}
const { brandingDataUrl: baseUrl } = config;
const fqn = extractFqnFromPath();
const fqn = extractFqnFromPath(state);
if (baseUrl && fqn) {
return `${baseUrl}?conferenceFqn=${encodeURIComponent(fqn)}`;

View File

@ -1,7 +1,7 @@
import { GiphyContent, GiphyGridView, GiphyMediaType } from '@giphy/react-native-sdk';
import React, { useCallback, useState } from 'react';
import { Image, Keyboard, Text, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useTranslation } from 'react-i18next';
import { Image, Text, View } from 'react-native';
import { useDispatch } from 'react-redux';
import { createGifSentEvent, sendAnalytics } from '../../../analytics';
@ -16,7 +16,7 @@ import styles from './styles';
const GifsMenu = () => {
const [ searchQuery, setSearchQuery ] = useState('');
const dispatch = useDispatch();
const insets = useSafeAreaInsets();
const { t } = useTranslation();
const content = searchQuery === ''
? GiphyContent.trending({ mediaType: GiphyMediaType.Gif })
@ -34,33 +34,32 @@ const GifsMenu = () => {
goBack();
}, []);
const onScroll = useCallback(Keyboard.dismiss, []);
return (<JitsiScreen
style = { styles.container }>
<ClearableInput
autoFocus = { true }
customStyles = { styles.clearableInput }
onChange = { setSearchQuery }
placeholder = 'Search GIPHY'
value = { searchQuery } />
<GiphyGridView
cellPadding = { 5 }
content = { content }
onMediaSelect = { sendGif }
onScroll = { onScroll }
style = { styles.grid } />
<View
style = { [ styles.credit, {
bottom: insets.bottom,
left: insets.left,
right: insets.right
} ] }>
const footerComponent = () => (
<View style = { styles.credit }>
<Text
style = { styles.creditText }>Powered by</Text>
<Image source = { require('../../../../../images/GIPHY_logo.png') } />
</View>
</JitsiScreen>);
);
return (
<JitsiScreen
/* eslint-disable-next-line react/jsx-no-bind */
footerComponent = { footerComponent }
style = { styles.container }>
<ClearableInput
customStyles = { styles.clearableInput }
onChange = { setSearchQuery }
placeholder = { t('giphy.search') }
value = { searchQuery } />
<GiphyGridView
cellPadding = { 5 }
content = { content }
onMediaSelect = { sendGif }
style = { styles.grid } />
</JitsiScreen>
);
};
export default GifsMenu;

View File

@ -22,15 +22,15 @@ export default {
},
credit: {
alignItems: 'center',
backgroundColor: BaseTheme.palette.ui01,
width: '100%',
height: 40,
position: 'absolute',
marginBottom: 0,
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center'
height: 56,
justifyContent: 'center',
marginBottom: BaseTheme.spacing[0],
paddingBottom: BaseTheme.spacing[4],
width: '100%'
},
creditText: {

View File

@ -71,7 +71,8 @@ export const SET_MEETING_HIGHLIGHT_BUTTON_STATE = 'SET_MEETING_HIGHLIGHT_BUTTON_
* Attempts to start the local recording.
*
* {
* type: START_LOCAL_RECORDING
* type: START_LOCAL_RECORDING,
* onlySelf: boolean
* }
*/
export const START_LOCAL_RECORDING = 'START_LOCAL_RECORDING';

View File

@ -338,11 +338,13 @@ function _setPendingRecordingNotificationUid(uid: ?number, streamType: string) {
/**
* Starts local recording.
*
* @param {boolean} onlySelf - Whether to only record the local streams.
* @returns {Object}
*/
export function startLocalVideoRecording() {
export function startLocalVideoRecording(onlySelf) {
return {
type: START_LOCAL_RECORDING
type: START_LOCAL_RECORDING,
onlySelf
};
}

View File

@ -139,6 +139,7 @@ class AbstractStartRecordingDialog extends Component<Props, State> {
= this._onSelectedRecordingServiceChanged.bind(this);
this._onSharingSettingChanged = this._onSharingSettingChanged.bind(this);
this._toggleScreenshotCapture = this._toggleScreenshotCapture.bind(this);
this._onLocalRecordingSelfChange = this._onLocalRecordingSelfChange.bind(this);
let selectedRecordingService;
@ -157,7 +158,8 @@ class AbstractStartRecordingDialog extends Component<Props, State> {
userName: undefined,
sharingEnabled: true,
spaceLeft: undefined,
selectedRecordingService
selectedRecordingService,
localRecordingOnlySelf: false
};
}
@ -211,6 +213,19 @@ class AbstractStartRecordingDialog extends Component<Props, State> {
});
}
_onLocalRecordingSelfChange: () => void;
/**
* Callback to handle local recording only self setting change.
*
* @returns {void}
*/
_onLocalRecordingSelfChange() {
this.setState({
localRecordingOnlySelf: !this.state.localRecordingOnlySelf
});
}
_onSelectedRecordingServiceChanged: (string) => void;
/**
@ -326,7 +341,7 @@ class AbstractStartRecordingDialog extends Component<Props, State> {
break;
}
case RECORDING_TYPES.LOCAL: {
dispatch(startLocalVideoRecording());
dispatch(startLocalVideoRecording(this.state.localRecordingOnlySelf));
return true;
}
@ -390,8 +405,7 @@ class AbstractStartRecordingDialog extends Component<Props, State> {
export function mapStateToProps(state: Object) {
const {
autoCaptionOnRecord = false,
fileRecordingsServiceEnabled = false,
fileRecordingsServiceSharingEnabled = false,
recordingService,
dropbox = {}
} = state['features/base/config'];
@ -399,8 +413,8 @@ export function mapStateToProps(state: Object) {
_appKey: dropbox.appKey,
_autoCaptionOnRecord: autoCaptionOnRecord,
_conference: state['features/base/conference'].conference,
_fileRecordingsServiceEnabled: fileRecordingsServiceEnabled,
_fileRecordingsServiceSharingEnabled: fileRecordingsServiceSharingEnabled,
_fileRecordingsServiceEnabled: recordingService?.enabled ?? false,
_fileRecordingsServiceSharingEnabled: recordingService?.sharingEnabled ?? false,
_isDropboxEnabled: isDropboxEnabled(state),
_rToken: state['features/dropbox'].rToken,
_tokenExpireDate: state['features/dropbox'].expireDate,

View File

@ -7,6 +7,7 @@ import {
sendAnalytics
} from '../../../analytics';
import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
import { setVideoMuted } from '../../../base/media';
import { stopLocalVideoRecording } from '../../actions';
import { getActiveSession } from '../../functions';
@ -38,6 +39,11 @@ export type Props = {
*/
dispatch: Function,
/**
* The user trying to stop the video while local recording is running.
*/
localRecordingVideoStop?: boolean,
/**
* Invoked to obtain translated strings.
*/
@ -78,6 +84,9 @@ export default class AbstractStopRecordingDialog<P: Props>
if (this.props._localRecording) {
this.props.dispatch(stopLocalVideoRecording());
if (this.props.localRecordingVideoStop) {
this.props.dispatch(setVideoMuted(true));
}
} else {
const { _fileRecordingSession } = this.props;

View File

@ -6,16 +6,23 @@ import { getRoomName } from '../../../base/conference';
// @ts-ignore
import { MEDIA_TYPE } from '../../../base/media';
// @ts-ignore
import { getTrackState } from '../../../base/tracks';
import { getTrackState, getLocalTrack } from '../../../base/tracks';
import { inIframe } from '../../../base/util/iframeUtils';
// @ts-ignore
import { stopLocalVideoRecording } from '../../actions.any';
declare var APP: any;
interface IReduxStore {
dispatch: Function;
getState: Function;
}
interface SelfRecording {
on: boolean;
withVideo: boolean;
}
interface ILocalRecordingManager {
recordingData: Blob[];
recorder: MediaRecorder|undefined;
@ -30,9 +37,10 @@ interface ILocalRecordingManager {
getFilename: () => string;
saveRecording: (recordingData: Blob[], filename: string) => void;
stopLocalRecording: () => void;
startLocalRecording: (store: IReduxStore) => void;
startLocalRecording: (store: IReduxStore, onlySelf: boolean) => void;
isRecordingLocally: () => boolean;
totalSize: number;
selfRecording: SelfRecording;
}
const getMimeType = (): string => {
@ -63,6 +71,10 @@ const LocalRecordingManager: ILocalRecordingManager = {
audioDestination: undefined,
roomName: '',
totalSize: 1073741824, // 1GB in bytes
selfRecording: {
on: false,
withVideo: false
},
get mediaType() {
if (!preferredMediaType) {
@ -93,6 +105,9 @@ const LocalRecordingManager: ILocalRecordingManager = {
* Adds audio track to the recording stream.
*/
addAudioTrackToLocalRecording(track) {
if(this.selfRecording.on) {
return;
}
if (track) {
const stream = new MediaStream([ track ]);
@ -143,58 +158,85 @@ const LocalRecordingManager: ILocalRecordingManager = {
/**
* Starts a local recording.
*/
async startLocalRecording(store) {
async startLocalRecording(store, onlySelf) {
const { dispatch, getState } = store;
// @ts-ignore
const supportsCaptureHandle = Boolean(navigator.mediaDevices.setCaptureHandleConfig) && !inIframe();
const tabId = uuidV4();
if (supportsCaptureHandle) {
// @ts-ignore
navigator.mediaDevices.setCaptureHandleConfig({
handle: `JitsiMeet-${tabId}`,
permittedOrigins: [ '*' ]
});
}
this.selfRecording.on = onlySelf;
this.recordingData = [];
// @ts-ignore
const gdmStream = await navigator.mediaDevices.getDisplayMedia({
// @ts-ignore
video: { displaySurface: 'browser', frameRate: 30 },
audio: {
autoGainControl: false,
channelCount: 2,
echoCancellation: false,
noiseSuppression: false
}
});
// @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());
let gdmStream: MediaStream = new MediaStream();
const tracks = getTrackState(getState());
tracks.forEach((track: any) => {
if (track.mediaType === MEDIA_TYPE.AUDIO) {
const audioTrack = track?.jitsiTrack?.track;
this.addAudioTrackToLocalRecording(audioTrack);
if(onlySelf) {
let audioTrack: MediaStreamTrack | undefined = getLocalTrack(tracks, MEDIA_TYPE.AUDIO)?.jitsiTrack?.track;
let videoTrack: MediaStreamTrack | undefined = getLocalTrack(tracks, MEDIA_TYPE.VIDEO)?.jitsiTrack?.track;
if(!audioTrack) {
APP.conference.muteAudio(false);
setTimeout(() => APP.conference.muteAudio(true), 100);
await new Promise((resolve) => {
setTimeout(resolve, 100);
});
}
if(videoTrack && videoTrack.readyState !== 'live') {
videoTrack = undefined;
}
audioTrack = getLocalTrack(getTrackState(getState()), MEDIA_TYPE.AUDIO)?.jitsiTrack?.track;
if(!audioTrack && !videoTrack) {
throw new Error('NoLocalStreams')
}
this.selfRecording.withVideo = Boolean(videoTrack);
const localTracks = [];
audioTrack && localTracks.push(audioTrack);
videoTrack && localTracks.push(videoTrack);
this.stream = new MediaStream(localTracks);
} else {
if (supportsCaptureHandle) {
// @ts-ignore
navigator.mediaDevices.setCaptureHandleConfig({
handle: `JitsiMeet-${tabId}`,
permittedOrigins: [ '*' ]
});
}
});
this.stream = new MediaStream([
...(this.audioDestination?.stream.getAudioTracks() || []),
gdmStream.getVideoTracks()[0]
]);
// @ts-ignore
gdmStream = await navigator.mediaDevices.getDisplayMedia({
// @ts-ignore
video: { displaySurface: 'browser', frameRate: 30 },
audio: {
autoGainControl: false,
channelCount: 2,
echoCancellation: false,
noiseSuppression: false
}
});
// @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);
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
@ -209,18 +251,20 @@ const LocalRecordingManager: ILocalRecordingManager = {
}
});
this.recorder.addEventListener('stop', () => {
this.stream?.getTracks().forEach((track: MediaStreamTrack) => track.stop());
gdmStream.getTracks().forEach((track: MediaStreamTrack) => track.stop());
});
if(!onlySelf) {
this.recorder.addEventListener('stop', () => {
this.stream?.getTracks().forEach((track: MediaStreamTrack) => track.stop());
gdmStream?.getTracks().forEach((track: MediaStreamTrack) => track.stop());
});
gdmStream.addEventListener('inactive', () => {
dispatch(stopLocalVideoRecording());
});
gdmStream?.addEventListener('inactive', () => {
dispatch(stopLocalVideoRecording());
});
this.stream.addEventListener('inactive', () => {
dispatch(stopLocalVideoRecording());
});
this.stream.addEventListener('inactive', () => {
dispatch(stopLocalVideoRecording());
});
}
this.recorder.start(5000);
},

View File

@ -44,6 +44,11 @@ type Props = {
*/
_dialogStyles: StyleType,
/**
* Whether to hide the storage warning or not.
*/
_hideStorageWarning: boolean,
/**
* Whether local recording is enabled or not.
*/
@ -96,12 +101,22 @@ type Props = {
*/
isVpaas: boolean,
/**
* Whether or not we should only record the local streams.
*/
localRecordingOnlySelf: boolean,
/**
* The function will be called when there are changes related to the
* switches.
*/
onChange: Function,
/**
* Callback to change the local recording only self setting.
*/
onLocalRecordingSelfChange: Function,
/**
* Callback to be invoked on sharing setting change.
*/
@ -201,9 +216,15 @@ class StartRecordingDialogContent extends Component<Props> {
* @returns {boolean}
*/
_shouldRenderFileSharingContent() {
const { fileRecordingsServiceSharingEnabled, isVpaas, selectedRecordingService } = this.props;
const {
fileRecordingsServiceEnabled,
fileRecordingsServiceSharingEnabled,
isVpaas,
selectedRecordingService
} = this.props;
if (!fileRecordingsServiceSharingEnabled
if (!fileRecordingsServiceEnabled
|| !fileRecordingsServiceSharingEnabled
|| isVpaas
|| selectedRecordingService !== RECORDING_TYPES.JITSI_REC_SERVICE) {
return false;
@ -270,13 +291,14 @@ class StartRecordingDialogContent extends Component<Props> {
_renderUploadToTheCloudInfo() {
const {
_dialogStyles,
_hideStorageWarning,
_styles: styles,
isVpaas,
selectedRecordingService,
t
} = this.props;
if (!(isVpaas && selectedRecordingService === RECORDING_TYPES.JITSI_REC_SERVICE)) {
if (!(isVpaas && selectedRecordingService === RECORDING_TYPES.JITSI_REC_SERVICE) || _hideStorageWarning) {
return null;
}
@ -308,9 +330,8 @@ class StartRecordingDialogContent extends Component<Props> {
*/
_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)) {
// is enabled
if (!this.props.fileRecordingsServiceEnabled) {
return false;
}
@ -629,45 +650,76 @@ class StartRecordingDialogContent extends Component<Props> {
}
return (
<Container>
<Container
className = 'recording-header recording-header-line'
style = { styles.header }>
<>
<Container>
<Container
className = 'recording-icon-container'>
<Image
className = 'recording-icon'
src = { LOCAL_RECORDING }
style = { styles.recordingIcon } />
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>
<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
&& <>
{selectedRecordingService === RECORDING_TYPES.LOCAL && (
<>
<Container>
<Container
className = 'recording-header space-top'
style = { styles.header }>
<Container className = 'recording-icon-container file-sharing-icon-container'>
<Image
className = 'recording-file-sharing-icon'
src = { ICON_USERS }
style = { styles.recordingIcon } />
</Container>
<Text
className = 'recording-title'
style = {{
..._dialogStyles.text,
...styles.title
}}>
{t('recording.onlyRecordSelf')}
</Text>
<Switch
className = 'recording-switch'
disabled = { isValidating }
onValueChange = { this.props.onLocalRecordingSelfChange }
style = { styles.switch }
trackColor = {{ false: TRACK_COLOR }}
value = { this.props.localRecordingOnlySelf } />
</Container>
</Container>
<Text className = 'local-recording-warning text'>
{t('recording.localRecordingWarning')}
</Text>
{_localRecordingNoNotification && <Text className = 'local-recording-warning notification'>
{t('recording.localRecordingNoNotificationWarning')}
</Text>}
{_localRecordingNoNotification && !this.props.localRecordingOnlySelf
&& <Text className = 'local-recording-warning notification'>
{t('recording.localRecordingNoNotificationWarning')}
</Text>
}
</>
}
</Container>
)}
</>
);
}
@ -707,8 +759,9 @@ function _mapStateToProps(state) {
return {
..._abstractMapStateToProps(state),
isVpaas: isVpaasMeeting(state),
_localRecordingEnabled: state['features/base/config'].localRecording.enable,
_localRecordingNoNotification: !state['features/base/config'].localRecording.notifyAllParticipants,
_hideStorageWarning: state['features/base/config'].recording?.hideStorageWarning,
_localRecordingEnabled: !state['features/base/config'].localRecording?.disable,
_localRecordingNoNotification: !state['features/base/config'].localRecording?.notifyAllParticipants,
_styles: ColorSchemeRegistry.get(state, 'StartRecordingDialogContent')
};
}

View File

@ -55,6 +55,7 @@ class StartRecordingDialog extends AbstractStartRecordingDialog {
const {
isTokenValid,
isValidating,
localRecordingOnlySelf,
selectedRecordingService,
sharingEnabled,
spaceLeft,
@ -78,7 +79,9 @@ class StartRecordingDialog extends AbstractStartRecordingDialog {
integrationsEnabled = { this._areIntegrationsEnabled() }
isTokenValid = { isTokenValid }
isValidating = { isValidating }
localRecordingOnlySelf = { localRecordingOnlySelf }
onChange = { this._onSelectedRecordingServiceChanged }
onLocalRecordingSelfChange = { this._onLocalRecordingSelfChange }
onSharingSettingChanged = { this._onSharingSettingChanged }
selectedRecordingService = { selectedRecordingService }
sharingSetting = { sharingEnabled }
@ -105,6 +108,7 @@ class StartRecordingDialog extends AbstractStartRecordingDialog {
_onSubmit: () => boolean;
_onSelectedRecordingServiceChanged: (string) => void;
_onSharingSettingChanged: () => void;
_onLocalRecordingSelfChange: () => void;
}
/**

View File

@ -25,7 +25,7 @@ class StopRecordingDialog extends AbstractStopRecordingDialog<Props> {
* @returns {ReactElement}
*/
render() {
const { t } = this.props;
const { t, localRecordingVideoStop } = this.props;
return (
<Dialog
@ -33,7 +33,7 @@ class StopRecordingDialog extends AbstractStopRecordingDialog<Props> {
onSubmit = { this._onSubmit }
titleKey = 'dialog.recording'
width = 'small'>
{ t('dialog.stopRecordingWarning') }
{t(localRecordingVideoStop ? 'recording.localRecordingVideoStop' : 'dialog.stopRecordingWarning') }
</Dialog>
);
}

View File

@ -151,11 +151,19 @@ export function getRecordButtonProps(state: Object): ?string {
const isModerator = isLocalParticipantModerator(state);
const {
enableFeaturesBasedOnToken,
fileRecordingsEnabled
recordingService,
localRecording
} = state['features/base/config'];
const { features = {} } = getLocalParticipant(state);
let localRecordingEnabled = !localRecording?.disable;
visible = isModerator && fileRecordingsEnabled;
if (navigator.product === 'ReactNative') {
localRecordingEnabled = false;
}
const dropboxEnabled = isDropboxEnabled(state);
visible = isModerator && (recordingService?.enabled || localRecordingEnabled || dropboxEnabled);
if (enableFeaturesBasedOnToken) {
visible = visible && String(features.recording) === 'true';

View File

@ -133,27 +133,39 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => async action =>
case START_LOCAL_RECORDING: {
const { localRecording } = getState()['features/base/config'];
const { onlySelf } = action;
try {
await LocalRecordingManager.startLocalRecording({ dispatch,
getState });
getState }, action.onlySelf);
const props = {
descriptionKey: 'recording.on',
titleKey: 'dialog.recording'
};
if (localRecording.notifyAllParticipants) {
if (localRecording?.notifyAllParticipants && !onlySelf) {
dispatch(playSound(RECORDING_ON_SOUND_ID));
}
dispatch(showNotification(props, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
dispatch(updateLocalRecordingStatus(true));
sendAnalytics(createRecordingEvent('started', 'local'));
dispatch(showNotification({
titleKey: 'recording.localRecordingStartWarningTitle',
descriptionKey: 'recording.localRecordingStartWarning'
}, NOTIFICATION_TIMEOUT_TYPE.STICKY));
dispatch(updateLocalRecordingStatus(true, onlySelf));
sendAnalytics(createRecordingEvent('started', `local${onlySelf ? '.self' : ''}`));
} catch (err) {
logger.error('Capture failed', err);
const noTabError = err.message === 'WrongSurfaceSelected';
let descriptionKey = 'recording.error';
if (err.message === 'WrongSurfaceSelected') {
descriptionKey = 'recording.surfaceError';
} else if (err.message === 'NoLocalStreams') {
descriptionKey = 'recording.noStreams';
}
const props = {
descriptionKey: noTabError ? 'recording.surfaceError' : 'recording.error',
descriptionKey,
titleKey: 'recording.failedToStart'
};
@ -164,11 +176,12 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => async action =>
case STOP_LOCAL_RECORDING: {
const { localRecording } = getState()['features/base/config'];
const { onlySelf } = action;
if (LocalRecordingManager.isRecordingLocally()) {
LocalRecordingManager.stopLocalRecording();
dispatch(updateLocalRecordingStatus(false));
if (localRecording.notifyAllParticipants) {
if (localRecording?.notifyAllParticipants && !onlySelf) {
dispatch(playSound(RECORDING_OFF_SOUND_ID));
}
}