From 508f1e0da9cf003839488749f6066d340df5f0a9 Mon Sep 17 00:00:00 2001 From: Alex Bumbu Date: Fri, 5 Mar 2021 17:33:53 +0200 Subject: [PATCH] feat(iOS): screensharing support The Jitsi team would like to thank @AliKarpuzoglu, @linuxpi and The Hopp Foundation for the initial effort and help throughout. --- .../DarwinNotificationCenter.h | 31 +++ .../DarwinNotificationCenter.m | 50 +++++ .../JitsiMeetBroadcast Extension/Info.plist | 33 +++ .../JitsiMeetBroadcast Extension.entitlements | 10 + .../SampleHandler.h | 21 ++ .../SampleHandler.m | 123 ++++++++++++ .../SampleUploader.h | 33 +++ .../SampleUploader.m | 155 ++++++++++++++ .../SocketConnection.h | 34 ++++ .../SocketConnection.m | 189 ++++++++++++++++++ ios/app/app.entitlements | 4 + ios/app/app.xcodeproj/project.pbxproj | 183 +++++++++++++++++ ios/app/src/Info.plist | 12 +- ios/sdk/sdk.xcodeproj/project.pbxproj | 8 + ios/sdk/src/JitsiMeet.m | 5 + ios/sdk/src/ScheenshareEventEmiter.h | 25 +++ ios/sdk/src/ScheenshareEventEmiter.m | 63 ++++++ package-lock.json | 5 +- package.json | 2 +- .../mobile/picture-in-picture/functions.js | 4 +- .../native/ScreenSharingAndroidButton.js | 74 +++++++ .../components/native/ScreenSharingButton.js | 87 ++------ .../native/ScreenSharingIosButton.js | 132 ++++++++++++ 23 files changed, 1201 insertions(+), 82 deletions(-) create mode 100644 ios/app/JitsiMeetBroadcast Extension/DarwinNotificationCenter.h create mode 100644 ios/app/JitsiMeetBroadcast Extension/DarwinNotificationCenter.m create mode 100644 ios/app/JitsiMeetBroadcast Extension/Info.plist create mode 100644 ios/app/JitsiMeetBroadcast Extension/JitsiMeetBroadcast Extension.entitlements create mode 100644 ios/app/JitsiMeetBroadcast Extension/SampleHandler.h create mode 100644 ios/app/JitsiMeetBroadcast Extension/SampleHandler.m create mode 100644 ios/app/JitsiMeetBroadcast Extension/SampleUploader.h create mode 100644 ios/app/JitsiMeetBroadcast Extension/SampleUploader.m create mode 100644 ios/app/JitsiMeetBroadcast Extension/SocketConnection.h create mode 100644 ios/app/JitsiMeetBroadcast Extension/SocketConnection.m create mode 100644 ios/sdk/src/ScheenshareEventEmiter.h create mode 100644 ios/sdk/src/ScheenshareEventEmiter.m create mode 100644 react/features/toolbox/components/native/ScreenSharingAndroidButton.js create mode 100644 react/features/toolbox/components/native/ScreenSharingIosButton.js diff --git a/ios/app/JitsiMeetBroadcast Extension/DarwinNotificationCenter.h b/ios/app/JitsiMeetBroadcast Extension/DarwinNotificationCenter.h new file mode 100644 index 000000000..145f0d911 --- /dev/null +++ b/ios/app/JitsiMeetBroadcast Extension/DarwinNotificationCenter.h @@ -0,0 +1,31 @@ +/* + * Copyright @ 2021-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 + +NS_ASSUME_NONNULL_BEGIN + +extern NSNotificationName const kBroadcastStartedNotification; +extern NSNotificationName const kBroadcastStoppedNotification; + +@interface DarwinNotificationCenter: NSObject + ++ (instancetype)sharedInstance; +- (void)postNotificationWithName:(NSNotificationName)name; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ios/app/JitsiMeetBroadcast Extension/DarwinNotificationCenter.m b/ios/app/JitsiMeetBroadcast Extension/DarwinNotificationCenter.m new file mode 100644 index 000000000..65221515d --- /dev/null +++ b/ios/app/JitsiMeetBroadcast Extension/DarwinNotificationCenter.m @@ -0,0 +1,50 @@ +/* + * Copyright @ 2021-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 "DarwinNotificationCenter.h" + +NSNotificationName const kBroadcastStartedNotification = @"iOS_BroadcastStarted"; +NSNotificationName const kBroadcastStoppedNotification = @"iOS_BroadcastStopped"; + +@implementation DarwinNotificationCenter { + CFNotificationCenterRef _notificationCenter; +} + ++ (instancetype)sharedInstance { + static DarwinNotificationCenter *sharedInstance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedInstance = [[self alloc] init]; + }); + + return sharedInstance; +} + +- (instancetype)init { + self = [super init]; + if (self) { + _notificationCenter = CFNotificationCenterGetDarwinNotifyCenter(); + } + + return self; +} + +- (void)postNotificationWithName:(NSString*)name { + CFNotificationCenterPostNotification(_notificationCenter, (__bridge CFStringRef)name, NULL, NULL, true); +} + +@end + diff --git a/ios/app/JitsiMeetBroadcast Extension/Info.plist b/ios/app/JitsiMeetBroadcast Extension/Info.plist new file mode 100644 index 000000000..43bbbae72 --- /dev/null +++ b/ios/app/JitsiMeetBroadcast Extension/Info.plist @@ -0,0 +1,33 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + JitsiMeetBroadcast Extension + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + NSExtension + + NSExtensionPointIdentifier + com.apple.broadcast-services-upload + NSExtensionPrincipalClass + SampleHandler + RPBroadcastProcessMode + RPBroadcastProcessModeSampleBuffer + + + diff --git a/ios/app/JitsiMeetBroadcast Extension/JitsiMeetBroadcast Extension.entitlements b/ios/app/JitsiMeetBroadcast Extension/JitsiMeetBroadcast Extension.entitlements new file mode 100644 index 000000000..2bebc0bd1 --- /dev/null +++ b/ios/app/JitsiMeetBroadcast Extension/JitsiMeetBroadcast Extension.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.org.jitsi.meet.appgroup + + + diff --git a/ios/app/JitsiMeetBroadcast Extension/SampleHandler.h b/ios/app/JitsiMeetBroadcast Extension/SampleHandler.h new file mode 100644 index 000000000..f063375ec --- /dev/null +++ b/ios/app/JitsiMeetBroadcast Extension/SampleHandler.h @@ -0,0 +1,21 @@ +/* + * Copyright @ 2021-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 + +@interface SampleHandler : RPBroadcastSampleHandler + +@end diff --git a/ios/app/JitsiMeetBroadcast Extension/SampleHandler.m b/ios/app/JitsiMeetBroadcast Extension/SampleHandler.m new file mode 100644 index 000000000..e48140865 --- /dev/null +++ b/ios/app/JitsiMeetBroadcast Extension/SampleHandler.m @@ -0,0 +1,123 @@ +/* + * Copyright @ 2021-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 "SampleHandler.h" +#import "SocketConnection.h" +#import "SampleUploader.h" +#import "DarwinNotificationCenter.h" + +@interface SampleHandler () + +@property (nonatomic, retain) SocketConnection *clientConnection; +@property (nonatomic, retain) SampleUploader *uploader; + +@end + +@implementation SampleHandler + +- (instancetype)init { + self = [super init]; + if (self) { + self.clientConnection = [[SocketConnection alloc] initWithFilePath:self.socketFilePath]; + [self setupConnection]; + + self.uploader = [[SampleUploader alloc] initWithConnection:self.clientConnection]; + } + + return self; +} + +- (void)broadcastStartedWithSetupInfo:(NSDictionary *)setupInfo { + // User has requested to start the broadcast. Setup info from the UI extension can be supplied but optional. + NSLog(@"broadcast started"); + + [[DarwinNotificationCenter sharedInstance] postNotificationWithName:kBroadcastStartedNotification]; + [self openConnection]; +} + +- (void)broadcastPaused { + // User has requested to pause the broadcast. Samples will stop being delivered. +} + +- (void)broadcastResumed { + // User has requested to resume the broadcast. Samples delivery will resume. +} + +- (void)broadcastFinished { + // User has requested to finish the broadcast. + [[DarwinNotificationCenter sharedInstance] postNotificationWithName:kBroadcastStoppedNotification]; + [self.clientConnection close]; +} + +- (void)processSampleBuffer:(CMSampleBufferRef)sampleBuffer withType:(RPSampleBufferType)sampleBufferType { + static NSUInteger frameCount = 0; + switch (sampleBufferType) { + case RPSampleBufferTypeVideo: + // adjust frame rate by using every third frame + if (++frameCount%3 == 0 && self.uploader.isReady) { + [self.uploader sendSample:sampleBuffer]; + } + break; + + default: + break; + } +} + +// MARK: Private Methods + +- (NSString *)socketFilePath { + // the appGroupIdentifier must match the value provided in the app's info.plist for the RTCAppGroupIdentifier key + NSString *appGroupIdentifier = @"group.org.jitsi.meet.appgroup"; + NSURL *sharedContainer = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:appGroupIdentifier]; + NSString *socketFilePath = [[sharedContainer URLByAppendingPathComponent:@"rtc_SSFD"] path]; + + return socketFilePath; +} + +- (void)setupConnection { + __weak __typeof(self) weakSelf = self; + self.clientConnection.didClose = ^(NSError *error) { + NSLog(@"client connection did close: %@", error); + if (error) { + [weakSelf finishBroadcastWithError:error]; + } + else { + NSInteger JMScreenSharingStopped = 10001; + NSError *customError = [NSError errorWithDomain:RPRecordingErrorDomain + code:JMScreenSharingStopped + userInfo:@{NSLocalizedDescriptionKey: @"Screen sharing stopped"}]; + [weakSelf finishBroadcastWithError:customError]; + } + }; +} + +- (void)openConnection { + dispatch_queue_t queue = dispatch_queue_create("org.jitsi.meet.broadcast.connectTimer", 0); + dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); + dispatch_source_set_timer(timer, dispatch_walltime(NULL, 0), 0.1 * NSEC_PER_SEC, 0.1 * NSEC_PER_SEC); + + dispatch_source_set_event_handler(timer, ^{ + BOOL success = [self.clientConnection open]; + if (success) { + dispatch_source_cancel(timer); + } + }); + + dispatch_resume(timer); +} + +@end diff --git a/ios/app/JitsiMeetBroadcast Extension/SampleUploader.h b/ios/app/JitsiMeetBroadcast Extension/SampleUploader.h new file mode 100644 index 000000000..5685cdd75 --- /dev/null +++ b/ios/app/JitsiMeetBroadcast Extension/SampleUploader.h @@ -0,0 +1,33 @@ +/* + * Copyright @ 2021-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 +#import + +NS_ASSUME_NONNULL_BEGIN + +@class SocketConnection; + +@interface SampleUploader : NSObject + +@property (nonatomic, assign, readonly) BOOL isReady; + +- (instancetype)initWithConnection:(SocketConnection *)connection; +- (void)sendSample:(CMSampleBufferRef)sampleBuffer; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ios/app/JitsiMeetBroadcast Extension/SampleUploader.m b/ios/app/JitsiMeetBroadcast Extension/SampleUploader.m new file mode 100644 index 000000000..28e37274a --- /dev/null +++ b/ios/app/JitsiMeetBroadcast Extension/SampleUploader.m @@ -0,0 +1,155 @@ +/* + * Copyright @ 2021-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 +#import + +#import "SampleUploader.h" +#import "SocketConnection.h" + +static const NSInteger kBufferMaxLenght = 10 * 1024; + +@interface SampleUploader () + +@property (nonatomic, assign) BOOL isReady; + +@property (nonatomic, strong) dispatch_queue_t serialQueue; +@property (nonatomic, strong) SocketConnection *connection; +@property (nonatomic, strong) CIContext *imageContext; + +@property (nonatomic, strong) NSData *dataToSend; +@property (nonatomic, assign) NSUInteger byteIndex; + +@end + +@implementation SampleUploader + +- (instancetype)initWithConnection:(SocketConnection *)connection { + self = [super init]; + if (self) { + self.serialQueue = dispatch_queue_create("org.jitsi.meet.broadcast.sampleUploader", DISPATCH_QUEUE_SERIAL); + + self.connection = connection; + [self setupConnection]; + + self.imageContext = [[CIContext alloc] initWithOptions:nil]; + self.isReady = false; + } + + return self; +} + +- (void)sendSample:(CMSampleBufferRef)sampleBuffer { + self.isReady = false; + + self.dataToSend = [self prepareSample:sampleBuffer]; + self.byteIndex = 0; + + dispatch_async(self.serialQueue, ^{ + [self sendData]; + }); +} + +// MARK: Private Methods + +- (void)setupConnection { + __weak __typeof(self) weakSelf = self; + self.connection.didOpen = ^{ + weakSelf.isReady = true; + }; + self.connection.streamHasSpaceAvailable = ^{ + dispatch_async(weakSelf.serialQueue, ^{ + weakSelf.isReady = ![weakSelf sendData]; + }); + }; +} + +/** + This function downscales and converts to jpeg the provided sample buffer, then wraps the resulted image data into a CFHTTPMessageRef. Returns the serialized CFHTTPMessageRef. + */ +- (NSData *)prepareSample:(CMSampleBufferRef)sampleBuffer { + CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); + + CVPixelBufferLockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly); + + CGFloat scaleFactor = 2; + size_t width = CVPixelBufferGetWidth(imageBuffer)/scaleFactor; + size_t height = CVPixelBufferGetHeight(imageBuffer)/scaleFactor; + + CGAffineTransform scaleTransform = CGAffineTransformMakeScale(1/scaleFactor, 1/scaleFactor); + NSData *bufferData = [self jpegDataFromPixelBuffer:imageBuffer withScaling:scaleTransform]; + + CVPixelBufferUnlockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly); + + if (bufferData) { + CFHTTPMessageRef httpResponse = CFHTTPMessageCreateResponse(kCFAllocatorDefault, 200, NULL, kCFHTTPVersion1_1); + CFHTTPMessageSetHeaderFieldValue(httpResponse, (__bridge CFStringRef)@"Content-Length", (__bridge CFStringRef)[NSString stringWithFormat:@"%ld", bufferData.length]); + CFHTTPMessageSetHeaderFieldValue(httpResponse, (__bridge CFStringRef)@"Buffer-Width", (__bridge CFStringRef)[NSString stringWithFormat:@"%ld", width]); + CFHTTPMessageSetHeaderFieldValue(httpResponse, (__bridge CFStringRef)@"Buffer-Height", (__bridge CFStringRef)[NSString stringWithFormat:@"%ld", height]); + + CFHTTPMessageSetBody(httpResponse, (__bridge CFDataRef)bufferData); + + CFDataRef serializedMessage = CFHTTPMessageCopySerializedMessage(httpResponse); + CFRelease(httpResponse); + + return CFBridgingRelease(serializedMessage); + } + + return nil; +} + +- (BOOL)sendData { + if (!self.dataToSend) { + NSLog(@"no data to send"); + return false; + } + + NSUInteger bytesLeft = self.dataToSend.length - self.byteIndex; + + NSInteger length = bytesLeft > kBufferMaxLenght ? kBufferMaxLenght : bytesLeft; + uint8_t buffer[length]; + [self.dataToSend getBytes:&buffer range:NSMakeRange(self.byteIndex, length)]; + + length = [self.connection writeBufferToStream:buffer maxLength:length]; + if (length > 0) { + self.byteIndex += length; + bytesLeft -= length; + + if (bytesLeft == 0) { + NSLog(@"video sample processed successfully"); + self.dataToSend = nil; + self.byteIndex = 0; + } + } + else { + NSLog(@"writeBufferToStream failure"); + } + + return true; +} + +- (NSData *)jpegDataFromPixelBuffer:(CVPixelBufferRef)pixelBuffer withScaling:(CGAffineTransform)scaleTransform { + CIImage *image = [[CIImage alloc] initWithCVPixelBuffer:pixelBuffer]; + image = [image imageByApplyingTransform:scaleTransform]; + + NSDictionary *options = @{(NSString *)kCGImageDestinationLossyCompressionQuality: [NSNumber numberWithFloat:1.0]}; + NSData *imageData = [self.imageContext JPEGRepresentationOfImage:image + colorSpace:image.colorSpace + options:options]; + return imageData; +} + +@end diff --git a/ios/app/JitsiMeetBroadcast Extension/SocketConnection.h b/ios/app/JitsiMeetBroadcast Extension/SocketConnection.h new file mode 100644 index 000000000..7ba38943a --- /dev/null +++ b/ios/app/JitsiMeetBroadcast Extension/SocketConnection.h @@ -0,0 +1,34 @@ +/* + * Copyright @ 2021-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 + +NS_ASSUME_NONNULL_BEGIN + +@interface SocketConnection : NSObject + +@property (nonatomic, copy, nullable) void (^didOpen)(void); +@property (nonatomic, copy, nullable) void (^didClose)(NSError*); +@property (nonatomic, copy, nullable) void (^streamHasSpaceAvailable)(void); + +- (instancetype)initWithFilePath:(nonnull NSString *)filePath; +- (BOOL)open; +- (void)close; +- (NSInteger)writeBufferToStream:(const uint8_t*)buffer maxLength:(NSInteger)length; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ios/app/JitsiMeetBroadcast Extension/SocketConnection.m b/ios/app/JitsiMeetBroadcast Extension/SocketConnection.m new file mode 100644 index 000000000..7e92d9543 --- /dev/null +++ b/ios/app/JitsiMeetBroadcast Extension/SocketConnection.m @@ -0,0 +1,189 @@ +/* + * Copyright @ 2021-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 +#include + +#import "SocketConnection.h" + +@interface SocketConnection () + +@property (nonatomic, copy) NSString *filePath; + +@property (nonatomic, strong) NSInputStream *inputStream; +@property (nonatomic, strong) NSOutputStream *outputStream; + +@property (nonatomic, strong) NSThread *networkThread; + +@end + +@implementation SocketConnection { + int _socket; + struct sockaddr_un _socketAddr; +} + +- (instancetype)initWithFilePath:(NSString *)path { + self = [super init]; + if (self) { + self.filePath = path; + + [self setupSocketWithFilePath:path]; + [self setupNetworkThread]; + } + + return self; +} + +- (BOOL)open { + NSLog(@"Open socket connection"); + + if (![[NSFileManager defaultManager] fileExistsAtPath:self.filePath]) { + NSLog(@"failure: socket file missing"); + return false; + } + + int status = connect(_socket, (struct sockaddr *)&_socketAddr, sizeof(_socketAddr)); + if (status < 0) { + NSLog(@"failure: socket connect (%d)", status); + return false; + } + + [self.networkThread start]; + + CFReadStreamRef readStream; + CFWriteStreamRef writeStream; + + CFStreamCreatePairWithSocket(kCFAllocatorDefault, _socket, &readStream, &writeStream); + + self.inputStream = (__bridge_transfer NSInputStream *)readStream; + self.inputStream.delegate = self; + [self.inputStream setProperty:@"kCFBooleanTrue" forKey:@"kCFStreamPropertyShouldCloseNativeSocket"]; + + self.outputStream = (__bridge_transfer NSOutputStream *)writeStream; + self.outputStream.delegate = self; + [self.outputStream setProperty:@"kCFBooleanTrue" forKey:@"kCFStreamPropertyShouldCloseNativeSocket"]; + + [self performSelector:@selector(scheduleStreams) onThread:self.networkThread withObject:nil waitUntilDone:true]; + + [self.inputStream open]; + [self.outputStream open]; + + NSLog(@"read stream status: %ld", CFReadStreamGetStatus(readStream)); + NSLog(@"write stream status: %ld", CFWriteStreamGetStatus(writeStream)); + + return true; +} + +- (void)close { + [self performSelector:@selector(unscheduleStreams) onThread:self.networkThread withObject:nil waitUntilDone:true]; + + self.inputStream.delegate = nil; + self.outputStream.delegate = nil; + + [self.inputStream close]; + [self.outputStream close]; + + [self.networkThread cancel]; +} + +- (NSInteger)writeBufferToStream:(const uint8_t*)buffer maxLength:(NSInteger)length { + return [self.outputStream write:buffer maxLength:length]; +} + +// MARK: Private Methods + +- (BOOL)isOpen { + return self.inputStream.streamStatus == NSStreamStatusOpen && self.outputStream.streamStatus == NSStreamStatusOpen; +} + +- (void)setupSocketWithFilePath:(NSString*)path { + _socket = socket(AF_UNIX, SOCK_STREAM, 0); + + memset(&_socketAddr, 0, sizeof(_socketAddr)); + _socketAddr.sun_family = AF_UNIX; + strncpy(_socketAddr.sun_path, path.UTF8String, sizeof(_socketAddr.sun_path) - 1); +} + +- (void)setupNetworkThread { + self.networkThread = [[NSThread alloc] initWithBlock:^{ + do { + @autoreleasepool { + [[NSRunLoop currentRunLoop] run]; + } + } while (![NSThread currentThread].isCancelled); + }]; + self.networkThread.qualityOfService = NSQualityOfServiceUserInitiated; +} + +- (void)scheduleStreams { + [self.inputStream scheduleInRunLoop:NSRunLoop.currentRunLoop forMode:NSRunLoopCommonModes]; + [self.outputStream scheduleInRunLoop:NSRunLoop.currentRunLoop forMode:NSRunLoopCommonModes]; +} + +- (void)unscheduleStreams { + [self.inputStream removeFromRunLoop:NSRunLoop.currentRunLoop forMode:NSRunLoopCommonModes]; + [self.outputStream removeFromRunLoop:NSRunLoop.currentRunLoop forMode:NSRunLoopCommonModes]; +} + +- (void)notifyDidClose:(NSError *)error { + if (self.didClose) { + self.didClose(error); + } +} + +@end + +#pragma mark - NSStreamDelegate + +@implementation SocketConnection (NSStreamDelegate) + +- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode { + switch (eventCode) { + case NSStreamEventOpenCompleted: + NSLog(@"client stream open completed"); + if (aStream == self.outputStream && self.didOpen) { + self.didOpen(); + } + break; + case NSStreamEventHasBytesAvailable: + if (aStream == self.inputStream) { + uint8_t buffer; + NSInteger numberOfBytesRead = [(NSInputStream *)aStream read:&buffer maxLength:sizeof(buffer)]; + if (!numberOfBytesRead && aStream.streamStatus == NSStreamStatusAtEnd) { + NSLog(@"server socket closed"); + [self close]; + [self notifyDidClose:nil]; + } + } + break; + case NSStreamEventHasSpaceAvailable: + if (aStream == self.outputStream && self.streamHasSpaceAvailable) { + NSLog(@"client stream has space available"); + self.streamHasSpaceAvailable(); + } + break; + case NSStreamEventErrorOccurred: + NSLog(@"client stream error occurred: %@", aStream.streamError); + [self close]; + [self notifyDidClose:aStream.streamError]; + break; + + default: + break; + } +} + +@end diff --git a/ios/app/app.entitlements b/ios/app/app.entitlements index 1771a4ad3..c093ba626 100644 --- a/ios/app/app.entitlements +++ b/ios/app/app.entitlements @@ -8,6 +8,10 @@ applinks:beta.meet.jit.si applinks:meet.jit.si + com.apple.security.application-groups + + group.org.jitsi.meet.appgroup + com.apple.developer.siri diff --git a/ios/app/app.xcodeproj/project.pbxproj b/ios/app/app.xcodeproj/project.pbxproj index 032ce44f5..b76776111 100644 --- a/ios/app/app.xcodeproj/project.pbxproj +++ b/ios/app/app.xcodeproj/project.pbxproj @@ -23,6 +23,12 @@ 13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB11A68108700A75B9A /* LaunchScreen.xib */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; + 4E51B75E25E4115F0038575A /* DarwinNotificationCenter.m in Sources */ = {isa = PBXBuildFile; fileRef = 4E51B75D25E4115F0038575A /* DarwinNotificationCenter.m */; }; + 4EC49BB725BEDAC100E76218 /* ReplayKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4EC49B8625BED71300E76218 /* ReplayKit.framework */; }; + 4EC49BBB25BEDAC100E76218 /* SampleHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 4EC49BBA25BEDAC100E76218 /* SampleHandler.m */; }; + 4EC49BBF25BEDAC100E76218 /* JitsiMeetBroadcast Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 4EC49BB625BEDAC100E76218 /* JitsiMeetBroadcast Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 4EC49BCB25BEDB6400E76218 /* SocketConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = 4EC49BCA25BEDB6400E76218 /* SocketConnection.m */; }; + 4EC49BD125BF19CF00E76218 /* SampleUploader.m in Sources */ = {isa = PBXBuildFile; fileRef = 4EC49BD025BF19CF00E76218 /* SampleUploader.m */; }; 55BEDABDA92D47D399A70A5E /* libPods-JitsiMeet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D878B07B3FBD6E305EAA6B27 /* libPods-JitsiMeet.a */; }; DE050389256E904600DEE3A5 /* WebRTC.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE050388256E904600DEE3A5 /* WebRTC.xcframework */; }; DE05038A256E904600DEE3A5 /* WebRTC.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DE050388256E904600DEE3A5 /* WebRTC.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -48,6 +54,13 @@ remoteGlobalIDString = 0BEA5C241F7B8F73000D0AB4; remoteInfo = JitsiMeetCompanion; }; + 4EC49BBD25BEDAC100E76218 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 4EC49BB525BEDAC100E76218; + remoteInfo = "JitsiMeetBroadcast Extension"; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -85,6 +98,17 @@ name = "Embed Watch Content"; runOnlyForDeploymentPostprocessing = 0; }; + 4EC49B9025BED71300E76218 /* Embed App Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 4EC49BBF25BEDAC100E76218 /* JitsiMeetBroadcast Extension.appex in Embed App Extensions */, + ); + name = "Embed App Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ @@ -115,6 +139,18 @@ 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 4670A512A688E2DC34528282 /* Pods-jitsi-meet.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-jitsi-meet.debug.xcconfig"; path = "../Pods/Target Support Files/Pods-jitsi-meet/Pods-jitsi-meet.debug.xcconfig"; sourceTree = ""; }; + 4E51B75C25E4115F0038575A /* DarwinNotificationCenter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DarwinNotificationCenter.h; sourceTree = ""; }; + 4E51B75D25E4115F0038575A /* DarwinNotificationCenter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DarwinNotificationCenter.m; sourceTree = ""; }; + 4EC49B8625BED71300E76218 /* ReplayKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ReplayKit.framework; path = System/Library/Frameworks/ReplayKit.framework; sourceTree = SDKROOT; }; + 4EC49BB625BEDAC100E76218 /* JitsiMeetBroadcast Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "JitsiMeetBroadcast Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; + 4EC49BB925BEDAC100E76218 /* SampleHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SampleHandler.h; sourceTree = ""; }; + 4EC49BBA25BEDAC100E76218 /* SampleHandler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SampleHandler.m; sourceTree = ""; }; + 4EC49BBC25BEDAC100E76218 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 4EC49BC925BEDB6400E76218 /* SocketConnection.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SocketConnection.h; sourceTree = ""; }; + 4EC49BCA25BEDB6400E76218 /* SocketConnection.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SocketConnection.m; sourceTree = ""; }; + 4EC49BCF25BF19CF00E76218 /* SampleUploader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SampleUploader.h; sourceTree = ""; }; + 4EC49BD025BF19CF00E76218 /* SampleUploader.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SampleUploader.m; sourceTree = ""; }; + 4EC49BDB25BF280A00E76218 /* JitsiMeetBroadcast Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "JitsiMeetBroadcast Extension.entitlements"; sourceTree = ""; }; 609CB2080B75F75A89923F3D /* 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 = ""; }; B3B083EB1D4955FF0069CEE7 /* app.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = app.entitlements; sourceTree = ""; }; D878B07B3FBD6E305EAA6B27 /* libPods-JitsiMeet.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-JitsiMeet.a"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -153,6 +189,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 4EC49BB325BEDAC100E76218 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 4EC49BB725BEDAC100E76218 /* ReplayKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -165,6 +209,7 @@ DEFDBBDB25656E3B00344B23 /* WebRTC.xcframework */, 0BD6B4361EF82A6B00D1F4CD /* WebRTC.framework */, D878B07B3FBD6E305EAA6B27 /* libPods-JitsiMeet.a */, + 4EC49B8625BED71300E76218 /* ReplayKit.framework */, ); name = Frameworks; sourceTree = ""; @@ -216,6 +261,23 @@ path = src; sourceTree = ""; }; + 4EC49BB825BEDAC100E76218 /* JitsiMeetBroadcast Extension */ = { + isa = PBXGroup; + children = ( + 4EC49BDB25BF280A00E76218 /* JitsiMeetBroadcast Extension.entitlements */, + 4EC49BB925BEDAC100E76218 /* SampleHandler.h */, + 4EC49BBA25BEDAC100E76218 /* SampleHandler.m */, + 4EC49BC925BEDB6400E76218 /* SocketConnection.h */, + 4EC49BCA25BEDB6400E76218 /* SocketConnection.m */, + 4EC49BCF25BF19CF00E76218 /* SampleUploader.h */, + 4EC49BD025BF19CF00E76218 /* SampleUploader.m */, + 4EC49BBC25BEDAC100E76218 /* Info.plist */, + 4E51B75C25E4115F0038575A /* DarwinNotificationCenter.h */, + 4E51B75D25E4115F0038575A /* DarwinNotificationCenter.m */, + ); + path = "JitsiMeetBroadcast Extension"; + sourceTree = ""; + }; 5E96ADD5E49F3B3822EF9A52 /* Pods */ = { isa = PBXGroup; children = ( @@ -236,6 +298,7 @@ 13B07FAE1A68108700A75B9A /* src */, 5E96ADD5E49F3B3822EF9A52 /* Pods */, 0BEA5C261F7B8F73000D0AB4 /* Watch app */, + 4EC49BB825BEDAC100E76218 /* JitsiMeetBroadcast Extension */, 0BEA5C351F7B8F73000D0AB4 /* WatchKit extension */, ); indentWidth = 2; @@ -248,6 +311,7 @@ 13B07F961A680F5B00A75B9A /* jitsi-meet.app */, 0BEA5C251F7B8F73000D0AB4 /* JitsiMeetCompanion.app */, 0BEA5C311F7B8F73000D0AB4 /* JitsiMeetCompanion Extension.appex */, + 4EC49BB625BEDAC100E76218 /* JitsiMeetBroadcast Extension.appex */, ); name = Products; sourceTree = ""; @@ -305,17 +369,36 @@ DE11877A21EE09640078D059 /* Setup Google reverse URL handler */, DE4F6D6E22005C0400DE699E /* Setup Dropbox */, 0BEA5C491F7B8F73000D0AB4 /* Embed Watch Content */, + 4EC49B9025BED71300E76218 /* Embed App Extensions */, ); buildRules = ( ); dependencies = ( 0BEA5C401F7B8F73000D0AB4 /* PBXTargetDependency */, + 4EC49BBE25BEDAC100E76218 /* PBXTargetDependency */, ); name = JitsiMeet; productName = "Jitsi Meet"; productReference = 13B07F961A680F5B00A75B9A /* jitsi-meet.app */; productType = "com.apple.product-type.application"; }; + 4EC49BB525BEDAC100E76218 /* JitsiMeetBroadcast Extension */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4EC49BC025BEDAC100E76218 /* Build configuration list for PBXNativeTarget "JitsiMeetBroadcast Extension" */; + buildPhases = ( + 4EC49BB225BEDAC100E76218 /* Sources */, + 4EC49BB325BEDAC100E76218 /* Frameworks */, + 4EC49BB425BEDAC100E76218 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "JitsiMeetBroadcast Extension"; + productName = "JitsiMeetBroadcast Extension"; + productReference = 4EC49BB625BEDAC100E76218 /* JitsiMeetBroadcast Extension.appex */; + productType = "com.apple.product-type.app-extension"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -347,6 +430,9 @@ }; }; }; + 4EC49BB525BEDAC100E76218 = { + CreatedOnToolsVersion = 12.2; + }; }; }; buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "app" */; @@ -365,6 +451,7 @@ 13B07F861A680F5B00A75B9A /* JitsiMeet */, 0BEA5C241F7B8F73000D0AB4 /* JitsiMeetCompanion */, 0BEA5C301F7B8F73000D0AB4 /* JitsiMeetCompanion Extension */, + 4EC49BB525BEDAC100E76218 /* JitsiMeetBroadcast Extension */, ); }; /* End PBXProject section */ @@ -397,6 +484,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 4EC49BB425BEDAC100E76218 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -532,6 +626,17 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 4EC49BB225BEDAC100E76218 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4EC49BCB25BEDB6400E76218 /* SocketConnection.m in Sources */, + 4EC49BBB25BEDAC100E76218 /* SampleHandler.m in Sources */, + 4E51B75E25E4115F0038575A /* DarwinNotificationCenter.m in Sources */, + 4EC49BD125BF19CF00E76218 /* SampleUploader.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -545,6 +650,11 @@ target = 0BEA5C241F7B8F73000D0AB4 /* JitsiMeetCompanion */; targetProxy = 0BEA5C3F1F7B8F73000D0AB4 /* PBXContainerItemProxy */; }; + 4EC49BBE25BEDAC100E76218 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 4EC49BB525BEDAC100E76218 /* JitsiMeetBroadcast Extension */; + targetProxy = 4EC49BBD25BEDAC100E76218 /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ @@ -770,6 +880,70 @@ }; name = Release; }; + 4EC49BC125BEDAC100E76218 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = "JitsiMeetBroadcast Extension/JitsiMeetBroadcast Extension.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = FC967L3QRG; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = "JitsiMeetBroadcast Extension/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = org.jitsi.meet.broadcast.extension; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 4EC49BC225BEDAC100E76218 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = "JitsiMeetBroadcast Extension/JitsiMeetBroadcast Extension.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = FC967L3QRG; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = "JitsiMeetBroadcast Extension/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = org.jitsi.meet.broadcast.extension; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; 83CBBA201A601CBA00E9B192 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -914,6 +1088,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 4EC49BC025BEDAC100E76218 /* Build configuration list for PBXNativeTarget "JitsiMeetBroadcast Extension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4EC49BC125BEDAC100E76218 /* Debug */, + 4EC49BC225BEDAC100E76218 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "app" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/ios/app/src/Info.plist b/ios/app/src/Info.plist index 4468307fa..b05ce4cde 100644 --- a/ios/app/src/Info.plist +++ b/ios/app/src/Info.plist @@ -45,6 +45,8 @@ CFBundleVersion 1 + FirebaseCrashlyticsCollectionEnabled + false FirebaseScreenReportingEnabled ITSAppUsesNonExemptEncryption @@ -66,14 +68,18 @@ See your scheduled meetings in the app. NSCameraUsageDescription Participate in meetings with video. - NSMicrophoneUsageDescription - Participate in meetings with voice. NSLocalNetworkUsageDescription Local network is used for establishing Peer-to-Peer connections. + NSMicrophoneUsageDescription + Participate in meetings with voice. NSUserActivityTypes org.jitsi.JitsiMeet.ios.conference + RTCAppGroupIdentifier + group.org.jitsi.meet.appgroup + RTCScreenSharingExtension + org.jitsi.meet.broadcast.extension UIBackgroundModes audio @@ -99,7 +105,5 @@ UIViewControllerBasedStatusBarAppearance - FirebaseCrashlyticsCollectionEnabled - false diff --git a/ios/sdk/sdk.xcodeproj/project.pbxproj b/ios/sdk/sdk.xcodeproj/project.pbxproj index 8884347fa..6ed671b06 100644 --- a/ios/sdk/sdk.xcodeproj/project.pbxproj +++ b/ios/sdk/sdk.xcodeproj/project.pbxproj @@ -24,6 +24,8 @@ 0BCA49601EC4B6C600B793EE /* POSIX.m in Sources */ = {isa = PBXBuildFile; fileRef = 0BCA495D1EC4B6C600B793EE /* POSIX.m */; }; 0BCA49611EC4B6C600B793EE /* Proximity.m in Sources */ = {isa = PBXBuildFile; fileRef = 0BCA495E1EC4B6C600B793EE /* Proximity.m */; }; 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 */; }; 6C31EDC820C06D490089C899 /* recordingOn.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 6C31EDC720C06D490089C899 /* recordingOn.mp3 */; }; 6C31EDCA20C06D530089C899 /* recordingOff.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 6C31EDC920C06D530089C899 /* recordingOff.mp3 */; }; 6F08DF7D4458EE3CF3F36F6D /* libPods-JitsiMeetSDK.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E4376CA6886DE68FD7A4294B /* libPods-JitsiMeetSDK.a */; }; @@ -85,6 +87,8 @@ 0BD906E51EC0C00300C8C18E /* JitsiMeetSDK.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = JitsiMeetSDK.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 0BD906E81EC0C00300C8C18E /* JitsiMeet.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JitsiMeet.h; sourceTree = ""; }; 0BD906E91EC0C00300C8C18E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 4E51B76225E5345E0038575A /* ScheenshareEventEmiter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ScheenshareEventEmiter.h; sourceTree = ""; }; + 4E51B76325E5345E0038575A /* ScheenshareEventEmiter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ScheenshareEventEmiter.m; sourceTree = ""; }; 6C31EDC720C06D490089C899 /* recordingOn.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; name = recordingOn.mp3; path = ../../sounds/recordingOn.mp3; sourceTree = ""; }; 6C31EDC920C06D530089C899 /* recordingOff.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; name = recordingOff.mp3; path = ../../sounds/recordingOff.mp3; sourceTree = ""; }; 75635B0820751D6D00F29C9F /* joined.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; name = joined.wav; path = ../../sounds/joined.wav; sourceTree = ""; }; @@ -231,6 +235,8 @@ C8AFD27D2462C613000293D2 /* InfoPlistUtil.h */, C8AFD27E2462C613000293D2 /* InfoPlistUtil.m */, C81E9AB825AC5AD800B134D9 /* ExternalAPI.h */, + 4E51B76225E5345E0038575A /* ScheenshareEventEmiter.h */, + 4E51B76325E5345E0038575A /* ScheenshareEventEmiter.m */, ); path = src; sourceTree = ""; @@ -298,6 +304,7 @@ 0B93EF7E1EC9DDCD0030D24D /* RCTBridgeWrapper.h in Headers */, DE81A2DE2317ED5400AE1940 /* JitsiMeetBaseLogHandler.h in Headers */, DEA9F284258A5D9900D4CD74 /* JitsiMeetSDK.h in Headers */, + 4E51B76425E5345E0038575A /* ScheenshareEventEmiter.h in Headers */, DE65AACC2318028300290BEC /* JitsiMeetBaseLogHandler+Private.h in Headers */, 0B412F221EDEF6EA00B1A0A6 /* JitsiMeetViewDelegate.h in Headers */, 0BD906EA1EC0C00300C8C18E /* JitsiMeet.h in Headers */, @@ -466,6 +473,7 @@ C69EFA0C209A0F660027712B /* JMCallKitEmitter.swift in Sources */, DEFE535621FB2E8300011A3A /* ReactUtils.m in Sources */, C6A34261204EF76800E062DD /* DragGestureController.swift in Sources */, + 4E51B76525E5345E0038575A /* ScheenshareEventEmiter.m in Sources */, A4A934E9212F3ADB001E9388 /* Dropbox.m in Sources */, C69EFA0D209A0F660027712B /* JMCallKitProxy.swift in Sources */, DE81A2D52316AC4D00AE1940 /* JitsiMeetLogger.m in Sources */, diff --git a/ios/sdk/src/JitsiMeet.m b/ios/sdk/src/JitsiMeet.m index 8b8731f26..a7e4b8848 100644 --- a/ios/sdk/src/JitsiMeet.m +++ b/ios/sdk/src/JitsiMeet.m @@ -23,6 +23,7 @@ #import "RCTBridgeWrapper.h" #import "ReactUtils.h" #import "RNSplashScreen.h" +#import "ScheenshareEventEmiter.h" #import #import @@ -31,6 +32,7 @@ @implementation JitsiMeet { RCTBridgeWrapper *_bridgeWrapper; NSDictionary *_launchOptions; + ScheenshareEventEmiter *_screenshareEventEmiter; } #pragma mak - This class is a singleton @@ -50,6 +52,9 @@ if (self = [super init]) { // Initialize the on and only bridge for interfacing with React Native. _bridgeWrapper = [[RCTBridgeWrapper alloc] init]; + + // Initialize the listener for handling start/stop screensharing notifications. + _screenshareEventEmiter = [[ScheenshareEventEmiter alloc] init]; // Register a fatal error handler for React. registerReactFatalErrorHandler(); diff --git a/ios/sdk/src/ScheenshareEventEmiter.h b/ios/sdk/src/ScheenshareEventEmiter.h new file mode 100644 index 000000000..aa998efe0 --- /dev/null +++ b/ios/sdk/src/ScheenshareEventEmiter.h @@ -0,0 +1,25 @@ +/* + * Copyright @ 2021-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 + +NS_ASSUME_NONNULL_BEGIN + +@interface ScheenshareEventEmiter : NSObject + +@end + +NS_ASSUME_NONNULL_END diff --git a/ios/sdk/src/ScheenshareEventEmiter.m b/ios/sdk/src/ScheenshareEventEmiter.m new file mode 100644 index 000000000..c46bea8e1 --- /dev/null +++ b/ios/sdk/src/ScheenshareEventEmiter.m @@ -0,0 +1,63 @@ +/* + * Copyright @ 2021-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 "ScheenshareEventEmiter.h" +#import "JitsiMeet+Private.h" +#import "ExternalAPI.h" + +NSNotificationName const kBroadcastStartedNotification = @"iOS_BroadcastStarted"; +NSNotificationName const kBroadcastStoppedNotification = @"iOS_BroadcastStopped"; + +@implementation ScheenshareEventEmiter { + CFNotificationCenterRef _notificationCenter; +} + +- (instancetype)init { + self = [super init]; + if (self) { + _notificationCenter = CFNotificationCenterGetDarwinNotifyCenter(); + [self setupObserver]; + } + + return self; +} + +- (void)dealloc { + [self clearObserver]; +} + +// MARK: Private Methods + +- (void)setupObserver { + CFNotificationCenterAddObserver(_notificationCenter, (__bridge const void *)(self), broadcastToggleNotificationCallback, (__bridge CFStringRef)kBroadcastStartedNotification, NULL, CFNotificationSuspensionBehaviorDeliverImmediately); + CFNotificationCenterAddObserver(_notificationCenter, (__bridge const void *)(self), broadcastToggleNotificationCallback, (__bridge CFStringRef)kBroadcastStoppedNotification, NULL, CFNotificationSuspensionBehaviorDeliverImmediately); +} + +- (void)clearObserver { + CFNotificationCenterRemoveObserver(_notificationCenter, (__bridge const void *)(self), (__bridge CFStringRef)kBroadcastStartedNotification, NULL); + CFNotificationCenterRemoveObserver(_notificationCenter, (__bridge const void *)(self), (__bridge CFStringRef)kBroadcastStoppedNotification, NULL); +} + +void broadcastToggleNotificationCallback(CFNotificationCenterRef center, + void *observer, + CFStringRef name, + const void *object, + CFDictionaryRef userInfo) { + ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI]; + [externalAPI toggleScreenShare]; +} + +@end diff --git a/package-lock.json b/package-lock.json index f9db3f54b..66aa0ac14 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14071,9 +14071,8 @@ "integrity": "sha512-iqdJ1KpZbR4XGahgVmaeibB7kDhyMT7wrylINgJaYBY38IAiI0LF32VX1umO4pko6n21YF5I/kSeNQ+OXGqqow==" }, "react-native-webrtc": { - "version": "1.87.3", - "resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-1.87.3.tgz", - "integrity": "sha512-fWnaEHFCFD7YnPR95aaUqLQ5b4dY4av0qHjmwHXeLHGvGrVeWF1je9PNhet7PDHUIJa4GIYKB/8+co51SXm5dA==", + "version": "github:react-native-webrtc/react-native-webrtc#1066b92d48048d67ff23288d68ab1734ec364cab", + "from": "github:react-native-webrtc/react-native-webrtc#1066b92d48048d67ff23288d68ab1734ec364cab", "requires": { "base64-js": "^1.1.2", "cross-os": "^1.3.0", diff --git a/package.json b/package.json index cb1d8fa94..e6ccdc71d 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,7 @@ "react-native-svg-transformer": "0.14.3", "react-native-url-polyfill": "1.2.0", "react-native-watch-connectivity": "0.4.3", - "react-native-webrtc": "1.87.3", + "react-native-webrtc": "github:react-native-webrtc/react-native-webrtc#1066b92d48048d67ff23288d68ab1734ec364cab", "react-native-webview": "11.0.2", "react-native-youtube-iframe": "1.2.3", "react-redux": "7.1.0", diff --git a/react/features/mobile/picture-in-picture/functions.js b/react/features/mobile/picture-in-picture/functions.js index 619501da0..9204b8b54 100644 --- a/react/features/mobile/picture-in-picture/functions.js +++ b/react/features/mobile/picture-in-picture/functions.js @@ -11,5 +11,7 @@ import { NativeModules } from 'react-native'; export function setPictureInPictureDisabled(disabled: boolean) { const { PictureInPicture } = NativeModules; - PictureInPicture.setPictureInPictureDisabled(disabled); + if (PictureInPicture) { + PictureInPicture.setPictureInPictureDisabled(disabled); + } } diff --git a/react/features/toolbox/components/native/ScreenSharingAndroidButton.js b/react/features/toolbox/components/native/ScreenSharingAndroidButton.js new file mode 100644 index 000000000..a195c9a98 --- /dev/null +++ b/react/features/toolbox/components/native/ScreenSharingAndroidButton.js @@ -0,0 +1,74 @@ +// @flow + +import { translate } from '../../../base/i18n'; +import { IconShareDesktop } from '../../../base/icons'; +import { connect } from '../../../base/redux'; +import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components'; +import { toggleScreensharing, isLocalVideoTrackDesktop } from '../../../base/tracks'; + +/** + * The type of the React {@code Component} props of {@link ScreenSharingAndroidButton}. + */ +type Props = AbstractButtonProps & { + + /** + * Whether video is currently muted or not. + */ + _screensharing: boolean, + + /** + * The redux {@code dispatch} function. + */ + dispatch: Function +}; + +/** + * An implementation of a button for toggling screen sharing. + */ +class ScreenSharingAndroidButton extends AbstractButton { + accessibilityLabel = 'toolbar.accessibilityLabel.shareYourScreen'; + icon = IconShareDesktop; + label = 'toolbar.startScreenSharing'; + toggledLabel = 'toolbar.stopScreenSharing'; + + /** + * Handles clicking / pressing the button. + * + * @override + * @protected + * @returns {void} + */ + _handleClick() { + this.props.dispatch(toggleScreensharing()); + } + + /** + * Indicates whether this button is in toggled state or not. + * + * @override + * @protected + * @returns {boolean} + */ + _isToggled() { + return this.props._screensharing; + } +} + +/** + * Maps (parts of) the redux state to the associated props for the + * {@code ToggleCameraButton} component. + * + * @param {Object} state - The Redux state. + * @private + * @returns {{ + * _disabled: boolean, + * _screensharing: boolean + * }} + */ +function _mapStateToProps(state): Object { + return { + _screensharing: isLocalVideoTrackDesktop(state) + }; +} + +export default translate(connect(_mapStateToProps)(ScreenSharingAndroidButton)); diff --git a/react/features/toolbox/components/native/ScreenSharingButton.js b/react/features/toolbox/components/native/ScreenSharingButton.js index b9966d3e3..e75390068 100644 --- a/react/features/toolbox/components/native/ScreenSharingButton.js +++ b/react/features/toolbox/components/native/ScreenSharingButton.js @@ -1,77 +1,18 @@ -// @flow - +import React from 'react'; import { Platform } from 'react-native'; -import { translate } from '../../../base/i18n'; -import { IconShareDesktop } from '../../../base/icons'; -import { connect } from '../../../base/redux'; -import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components'; -import { toggleScreensharing, isLocalVideoTrackDesktop } from '../../../base/tracks'; +import ScreenSharingAndroidButton from './ScreenSharingAndroidButton.js'; +import ScreenSharingIosButton from './ScreenSharingIosButton.js'; -/** - * The type of the React {@code Component} props of {@link ScreenSharingButton}. - */ -type Props = AbstractButtonProps & { +const ScreenSharingButton = props => ( + <> + {Platform.OS === 'android' + && + } + {Platform.OS === 'ios' + && + } + +); - /** - * Whether video is currently muted or not. - */ - _screensharing: boolean, - - /** - * The redux {@code dispatch} function. - */ - dispatch: Function -}; - -/** - * An implementation of a button for toggling screen sharing. - */ -class ScreenSharingButton extends AbstractButton { - accessibilityLabel = 'toolbar.accessibilityLabel.shareYourScreen'; - icon = IconShareDesktop; - label = 'toolbar.startScreenSharing'; - toggledLabel = 'toolbar.stopScreenSharing'; - - /** - * Handles clicking / pressing the button. - * - * @override - * @protected - * @returns {void} - */ - _handleClick() { - this.props.dispatch(toggleScreensharing()); - } - - /** - * Indicates whether this button is in toggled state or not. - * - * @override - * @protected - * @returns {boolean} - */ - _isToggled() { - return this.props._screensharing; - } -} - -/** - * Maps (parts of) the redux state to the associated props for the - * {@code ToggleCameraButton} component. - * - * @param {Object} state - The Redux state. - * @private - * @returns {{ - * _disabled: boolean, - * _screensharing: boolean - * }} - */ -function _mapStateToProps(state): Object { - return { - _screensharing: isLocalVideoTrackDesktop(state), - visible: Platform.OS === 'android' - }; -} - -export default translate(connect(_mapStateToProps)(ScreenSharingButton)); +export default ScreenSharingButton; diff --git a/react/features/toolbox/components/native/ScreenSharingIosButton.js b/react/features/toolbox/components/native/ScreenSharingIosButton.js new file mode 100644 index 000000000..453c4f746 --- /dev/null +++ b/react/features/toolbox/components/native/ScreenSharingIosButton.js @@ -0,0 +1,132 @@ +// @flow + +import React from 'react'; +import { findNodeHandle, NativeModules, Platform } from 'react-native'; +import { ScreenCapturePickerView } from 'react-native-webrtc'; + +import { translate } from '../../../base/i18n'; +import { IconShareDesktop } from '../../../base/icons'; +import { connect } from '../../../base/redux'; +import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components'; +import { isLocalVideoTrackDesktop } from '../../../base/tracks'; + +/** + * The type of the React {@code Component} props of {@link ScreenSharingIosButton}. + */ +type Props = AbstractButtonProps & { + + /** + * Whether video is currently muted or not. + */ + _screensharing: boolean, + + /** + * The redux {@code dispatch} function. + */ + dispatch: Function +}; + +const styles = { + screenCapturePickerView: { + display: 'none' + } +}; + +/** + * An implementation of a button for toggling screen sharing on iOS. + */ +class ScreenSharingIosButton extends AbstractButton { + _nativeComponent: ?Object; + _setNativeComponent: Function; + + accessibilityLabel = 'toolbar.accessibilityLabel.shareYourScreen'; + icon = IconShareDesktop; + label = 'toolbar.startScreenSharing'; + toggledLabel = 'toolbar.stopScreenSharing'; + + /** + * Initializes a new {@code ScreenSharingIosButton} instance. + * + * @param {Object} props - The React {@code Component} props to initialize + * the new {@code ScreenSharingIosButton} instance with. + */ + constructor(props) { + super(props); + + this._nativeComponent = null; + + // Bind event handlers so they are only bound once per instance. + this._setNativeComponent = this._setNativeComponent.bind(this); + } + + /** + * Sets the internal reference to the React Component wrapping the + * {@code RPSystemBroadcastPickerView} component. + * + * @param {ReactComponent} component - React Component. + * @returns {void} + */ + _setNativeComponent(component) { + this._nativeComponent = component; + } + + /** + * Handles clicking / pressing the button. + * + * @override + * @protected + * @returns {void} + */ + _handleClick() { + const handle = findNodeHandle(this._nativeComponent); + + NativeModules.ScreenCapturePickerViewManager.show(handle); + } + + /** + * Indicates whether this button is in toggled state or not. + * + * @override + * @protected + * @returns {boolean} + */ + _isToggled() { + return this.props._screensharing; + } + + /** + * Helper function to be implemented by subclasses, which may return a + * new React Element to be appended at the end of the button. + * + * @protected + * @returns {ReactElement|null} + */ + _getElementAfter() { + return ( + + ); + } +} + +/** + * Maps (parts of) the redux state to the associated props for the + * {@code ScreenSharingIosButton} component. + * + * @param {Object} state - The Redux state. + * @private + * @returns {{ + * _disabled: boolean, + * }} + */ +function _mapStateToProps(state): Object { + return { + _screensharing: isLocalVideoTrackDesktop(state), + + // TODO: this should work on iOS 12 too, but our trick to show the picker doesn't work. + visible: Platform.OS === 'ios' && Platform.Version.split('.')[0] >= 14 + }; +} + +export default translate(connect(_mapStateToProps)(ScreenSharingIosButton));