mirror of https://github.com/jitsi/jitsi-meet
The Jitsi team would like to thank @AliKarpuzoglu, @linuxpi and The Hopp Foundation for the initial effort and help throughout.pull/8739/head
parent
dcda89012e
commit
508f1e0da9
@ -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 <Foundation/Foundation.h> |
||||
|
||||
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 |
@ -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 |
||||
|
@ -0,0 +1,33 @@ |
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
||||
<plist version="1.0"> |
||||
<dict> |
||||
<key>CFBundleDevelopmentRegion</key> |
||||
<string>$(DEVELOPMENT_LANGUAGE)</string> |
||||
<key>CFBundleDisplayName</key> |
||||
<string>JitsiMeetBroadcast Extension</string> |
||||
<key>CFBundleExecutable</key> |
||||
<string>$(EXECUTABLE_NAME)</string> |
||||
<key>CFBundleIdentifier</key> |
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> |
||||
<key>CFBundleInfoDictionaryVersion</key> |
||||
<string>6.0</string> |
||||
<key>CFBundleName</key> |
||||
<string>$(PRODUCT_NAME)</string> |
||||
<key>CFBundlePackageType</key> |
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string> |
||||
<key>CFBundleShortVersionString</key> |
||||
<string>1.0</string> |
||||
<key>CFBundleVersion</key> |
||||
<string>1</string> |
||||
<key>NSExtension</key> |
||||
<dict> |
||||
<key>NSExtensionPointIdentifier</key> |
||||
<string>com.apple.broadcast-services-upload</string> |
||||
<key>NSExtensionPrincipalClass</key> |
||||
<string>SampleHandler</string> |
||||
<key>RPBroadcastProcessMode</key> |
||||
<string>RPBroadcastProcessModeSampleBuffer</string> |
||||
</dict> |
||||
</dict> |
||||
</plist> |
@ -0,0 +1,10 @@ |
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
||||
<plist version="1.0"> |
||||
<dict> |
||||
<key>com.apple.security.application-groups</key> |
||||
<array> |
||||
<string>group.org.jitsi.meet.appgroup</string> |
||||
</array> |
||||
</dict> |
||||
</plist> |
@ -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 <ReplayKit/ReplayKit.h> |
||||
|
||||
@interface SampleHandler : RPBroadcastSampleHandler |
||||
|
||||
@end |
@ -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<NSString *,NSObject *> *)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 |
@ -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 <Foundation/Foundation.h> |
||||
#import <ReplayKit/ReplayKit.h> |
||||
|
||||
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 |
@ -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 <MessageUI/MessageUI.h> |
||||
#import <ReplayKit/ReplayKit.h> |
||||
|
||||
#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 |
@ -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 <Foundation/Foundation.h> |
||||
|
||||
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 |
@ -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 <sys/socket.h> |
||||
#include <sys/un.h> |
||||
|
||||
#import "SocketConnection.h" |
||||
|
||||
@interface SocketConnection () <NSStreamDelegate> |
||||
|
||||
@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 |
@ -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 <Foundation/Foundation.h> |
||||
|
||||
NS_ASSUME_NONNULL_BEGIN |
||||
|
||||
@interface ScheenshareEventEmiter : NSObject |
||||
|
||||
@end |
||||
|
||||
NS_ASSUME_NONNULL_END |
@ -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 |
@ -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<Props, *> { |
||||
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)); |
@ -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'; |
||||
|
||||
/** |
||||
* The type of the React {@code Component} props of {@link ScreenSharingButton}. |
||||
*/ |
||||
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 ScreenSharingButton extends AbstractButton<Props, *> { |
||||
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)); |
||||
import ScreenSharingAndroidButton from './ScreenSharingAndroidButton.js'; |
||||
import ScreenSharingIosButton from './ScreenSharingIosButton.js'; |
||||
|
||||
const ScreenSharingButton = props => ( |
||||
<> |
||||
{Platform.OS === 'android' |
||||
&& <ScreenSharingAndroidButton { ...props } /> |
||||
} |
||||
{Platform.OS === 'ios' |
||||
&& <ScreenSharingIosButton { ...props } /> |
||||
} |
||||
</> |
||||
); |
||||
|
||||
export default ScreenSharingButton; |
||||
|
@ -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<Props, *> { |
||||
_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 ( |
||||
<ScreenCapturePickerView |
||||
ref = { this._setNativeComponent } |
||||
style = { styles.screenCapturePickerView } /> |
||||
); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 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)); |
Loading…
Reference in new issue