Compare commits
11 Commits
jitihouse/
...
mobile-22.
Author | SHA1 | Date |
---|---|---|
Saúl Ibarra Corretgé | 071531cf14 | |
tmoldovan8x8 | 3185418df0 | |
Saúl Ibarra Corretgé | e66649b802 | |
Calin-Teodor | a6fd5fd294 | |
Alex Bumbu | 41544f5314 | |
Saúl Ibarra Corretgé | cfb944ee9b | |
Robert Pintilii | a4394e3022 | |
Robert Pintilii | aca7cc427c | |
Robert Pintilii | b9973f65a2 | |
Calin Chitu | 39437f6ac6 | |
tmoldovan8x8 | 8b6a1e4451 |
|
@ -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
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
<resources>
|
||||
<style name="JitsiMeetActivityStyle" parent="Theme.AppCompat.Light.NoActionBar"/>
|
||||
</resources>
|
30
config.js
30
config.js
|
@ -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.
|
||||
|
|
|
@ -19,6 +19,10 @@
|
|||
font-size: 14px;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
&.space-top {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.recording-header-line {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 */,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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",
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
import './middleware.any.js';
|
|
@ -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);
|
||||
});
|
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)}`;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
|
|
|
@ -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')
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue