From f40c4b1a5a604dcf4b842052d4f5ac9f1d226f3e Mon Sep 17 00:00:00 2001 From: bipmeet <92094057+bipmeet@users.noreply.github.com> Date: Fri, 19 Aug 2022 15:15:34 +0300 Subject: [PATCH 01/10] add log getusermedia --- src/getUserMedia.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/getUserMedia.ts b/src/getUserMedia.ts index 37a647407..3c32cf804 100644 --- a/src/getUserMedia.ts +++ b/src/getUserMedia.ts @@ -14,6 +14,7 @@ interface Constraints { } export default function getUserMedia(constraints: Constraints = {}) { + console.log("getuserMedia : ", constraints) // According to // https://www.w3.org/TR/mediacapture-streams/#dom-mediadevices-getusermedia, // the constraints argument is a dictionary of type MediaStreamConstraints. From dae4287130cbf49864b5a863c0168451f59a61d1 Mon Sep 17 00:00:00 2001 From: bipmeet <92094057+bipmeet@users.noreply.github.com> Date: Mon, 22 Aug 2022 10:46:20 +0300 Subject: [PATCH 02/10] vb first commit --- README.md | 2 +- ios/RCTWebRTC/ScreenCaptureController.m | 4 - ios/RCTWebRTC/ScreenCapturer.m | 7 - ios/RCTWebRTC/VideoSourceInterceptor.h | 21 ++ ios/RCTWebRTC/VideoSourceInterceptor.m | 333 ++++++++++++++++++ ios/RCTWebRTC/WebRTCModule+RTCMediaStream.m | 26 +- .../WebRTCModule+RTCPeerConnection.m | 2 - ios/RCTWebRTC/WebRTCModule.h | 2 + package.json | 2 +- src/RTCUtil.ts | 6 + src/getUserMedia.ts | 2 +- 11 files changed, 389 insertions(+), 18 deletions(-) create mode 100644 ios/RCTWebRTC/VideoSourceInterceptor.h create mode 100644 ios/RCTWebRTC/VideoSourceInterceptor.m diff --git a/README.md b/README.md index d7e4e2aee..49d38e4fb 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Everyone is welcome to our [Discourse community](https://react-native-webrtc.dis ## WebRTC Revision -* Currently used revision: [M100](https://github.com/jitsi/webrtc/releases/tag/v100.0.0) +* Currently used revision: [M94](https://github.com/jitsi/webrtc/releases/tag/v94.0.0) * Supported architectures * Android: armeabi-v7a, arm64-v8a, x86, x86_64 * iOS: arm64, x86_64 (for bitcode support, run [this script](https://github.com/react-native-webrtc/react-native-webrtc/blob/master/tools/downloadBitcode.sh)) diff --git a/ios/RCTWebRTC/ScreenCaptureController.m b/ios/RCTWebRTC/ScreenCaptureController.m index 33443e9ad..b94da4c9a 100644 --- a/ios/RCTWebRTC/ScreenCaptureController.m +++ b/ios/RCTWebRTC/ScreenCaptureController.m @@ -35,10 +35,6 @@ - (instancetype)initWithCapturer:(nonnull ScreenCapturer *)capturer { return self; } -- (void)dealloc { - [self.capturer stopCapture]; -} - - (void)startCapture { if (!self.appGroupIdentifier) { return; diff --git a/ios/RCTWebRTC/ScreenCapturer.m b/ios/RCTWebRTC/ScreenCapturer.m index 572f54d64..f9a962b1d 100644 --- a/ios/RCTWebRTC/ScreenCapturer.m +++ b/ios/RCTWebRTC/ScreenCapturer.m @@ -141,13 +141,6 @@ - (instancetype)initWithDelegate:(__weak id)delegate { return self; } -- (void)setConnection:(SocketConnection *)connection { - if (_connection != connection) { - [_connection close]; - _connection = connection; - } -} - - (void)startCaptureWithConnection:(SocketConnection *)connection { _startTimeStampNs = -1; diff --git a/ios/RCTWebRTC/VideoSourceInterceptor.h b/ios/RCTWebRTC/VideoSourceInterceptor.h new file mode 100644 index 000000000..76cd1acc1 --- /dev/null +++ b/ios/RCTWebRTC/VideoSourceInterceptor.h @@ -0,0 +1,21 @@ +// +// VideoSourceInterceptor.h +// react-native-webrtc +// +// Created by YAVUZ SELIM CAKIR on 18.06.2022. +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface VideoSourceInterceptor : NSObject + +@property(nonatomic, strong) RTCVideoSource *videoSource; + +- (instancetype)initWithVideoSource: (RTCVideoSource*) videoSource; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ios/RCTWebRTC/VideoSourceInterceptor.m b/ios/RCTWebRTC/VideoSourceInterceptor.m new file mode 100644 index 000000000..8174471bf --- /dev/null +++ b/ios/RCTWebRTC/VideoSourceInterceptor.m @@ -0,0 +1,333 @@ +// +// VideoSourceInterceptor.m +// react-native-webrtc +// +// Created by YAVUZ SELIM CAKIR on 18.06.2022. +// + +#import "VideoSourceInterceptor.h" +#import +#import +#import +#import + +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface VideoSourceInterceptor () + +@property (nonatomic) RTCVideoCapturer *capturer; +@property (nonatomic, strong) MLKSegmenter *segmenter; +@property (nonatomic) RTCVideoRotation rotation; +@property (nonatomic) int64_t timeStampNs; +@property (nonatomic) CVPixelBufferRef backgroundBuffer; +@property (nonatomic) CVPixelBufferRef rightRotatedBackgroundBuffer; +@property (nonatomic) CVPixelBufferRef leftRotatedBackgroundBuffer; +@property (nonatomic) CVPixelBufferRef upsideRotatedBackgroundBuffer; + +@end + +@implementation VideoSourceInterceptor + +NSString * const portraitBackgroundImageUrl = @"https://i.ibb.co/5RMCH5G/portrait.jpg"; +NSString * const rightRotatedBackgroundImageUrl = @"https://i.ibb.co/YNsR7St/rotated-Right.jpg"; +NSString * const leftRotatedBackgroundImageUrl = @"https://i.ibb.co/cwwSKFn/rotated-Left.jpg"; +NSString * const upsideBackgroundImageUrl = @"https://i.ibb.co/mcSJZQk/upside.jpg"; + +- (instancetype)initWithVideoSource: (RTCVideoSource*) videoSource { + if (self = [super init]) { + _videoSource = videoSource; + + MLKSelfieSegmenterOptions *options = [[MLKSelfieSegmenterOptions alloc] init]; + options.segmenterMode = MLKSegmenterModeStream; + options.shouldEnableRawSizeMask = NO; + + self.segmenter = [MLKSegmenter segmenterWithOptions:options]; + + _backgroundBuffer = [self pixelBufferFromImageUrl:portraitBackgroundImageUrl]; + _rightRotatedBackgroundBuffer = [self pixelBufferFromImageUrl:rightRotatedBackgroundImageUrl]; + _leftRotatedBackgroundBuffer = [self pixelBufferFromImageUrl:leftRotatedBackgroundImageUrl]; + _upsideRotatedBackgroundBuffer = [self pixelBufferFromImageUrl:upsideBackgroundImageUrl]; + } + return self; +} + +- (CVPixelBufferRef) pixelBufferFromImageUrl: (NSString *) imageURL +{ + NSURL *url = [NSURL URLWithString:imageURL]; + CGDataProviderRef dataProvider = CGDataProviderCreateWithURL((CFURLRef)url); + CGImageRef backgroundRef = CGImageCreateWithJPEGDataProvider(dataProvider, NULL, true, kCGRenderingIntentDefault); + + CVPixelBufferRef resultBuffer = NULL; + resultBuffer = [self pixelBufferFromCGImage:backgroundRef]; + + CGDataProviderRelease(dataProvider); + + return resultBuffer; +} + +- (CVPixelBufferRef) pixelBufferFromCGImage: (CGImageRef) image +{ + NSDictionary *options = @{ + (NSString*)kCVPixelBufferCGImageCompatibilityKey : @YES, + (NSString*)kCVPixelBufferCGBitmapContextCompatibilityKey : @YES, + }; + + CVPixelBufferRef pxbuffer = NULL; + + CVReturn status = CVPixelBufferCreate(kCFAllocatorDefault, CGImageGetWidth(image), + CGImageGetHeight(image), kCVPixelFormatType_32BGRA, (__bridge CFDictionaryRef) options, + &pxbuffer); + + NSParameterAssert(status == kCVReturnSuccess && pxbuffer != NULL); + + CVPixelBufferLockBaseAddress(pxbuffer, 0); + void *pxdata = CVPixelBufferGetBaseAddress(pxbuffer); + + CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB(); + CGContextRef context = CGBitmapContextCreate(pxdata, CGImageGetWidth(image), + CGImageGetHeight(image), 8, 4*CGImageGetWidth(image), rgbColorSpace, + kCGImageAlphaPremultipliedLast); + NSParameterAssert(context); + + CGContextConcatCTM(context, CGAffineTransformMakeRotation(-90 * M_PI / 180.0)); + CGContextTranslateCTM(context, -640, 0); + CGAffineTransform flipVertical = CGAffineTransformMake( 1, 0, 0, -1, 0, CGImageGetHeight(image) ); + CGContextConcatCTM(context, flipVertical); + + CGContextDrawImage(context, CGRectMake(0, 0, CGImageGetWidth(image), + CGImageGetHeight(image)), image); + CGColorSpaceRelease(rgbColorSpace); + CGContextRelease(context); + + CVPixelBufferUnlockBaseAddress(pxbuffer, 0); + return pxbuffer; +} + +- (void)capturer:(nonnull RTCVideoCapturer *)capturer didCaptureVideoFrame:(nonnull RTCVideoFrame *)frame { + + RTCCVPixelBuffer* pixelBufferr = (RTCCVPixelBuffer *)frame.buffer; + CVPixelBufferRef pixelBufferRef = pixelBufferr.pixelBuffer; + + self.rotation = frame.rotation; + self.timeStampNs = frame.timeStampNs; + self.capturer = capturer; + + CMSampleBufferRef sampleBuffer = [self getCMSampleBuffer:pixelBufferRef timeStamp:self.timeStampNs]; + + MLKVisionImage *image = [[MLKVisionImage alloc] initWithBuffer:sampleBuffer]; + image.orientation = [self imageOrientation]; + + NSError *error; + MLKSegmentationMask *mask = + [self.segmenter resultsInImage:image error:&error]; + if (error != nil) { + // Error. + return; + } + + [self applySegmentationMask:mask + toPixelBuffer:pixelBufferRef + rotation:self.rotation]; + + RTC_OBJC_TYPE(RTCCVPixelBuffer) *rtcPixelBuffer = + [[RTC_OBJC_TYPE(RTCCVPixelBuffer) alloc] initWithPixelBuffer:pixelBufferRef]; + + RTCI420Buffer *i420buffer = [rtcPixelBuffer toI420]; + + RTC_OBJC_TYPE(RTCVideoFrame) *processedFrame = + [[RTC_OBJC_TYPE(RTCVideoFrame) alloc] initWithBuffer:i420buffer + rotation:self.rotation + timeStampNs:self.timeStampNs]; + + [_videoSource capturer:self.capturer didCaptureVideoFrame:processedFrame]; + + CMSampleBufferInvalidate(sampleBuffer); + CFRelease(sampleBuffer); + sampleBuffer = NULL; +} + +- (CMSampleBufferRef)getCMSampleBuffer: (CVPixelBufferRef)pixelBuffer timeStamp: (int64_t) timeStampNs { + + CMSampleTimingInfo info = kCMTimingInfoInvalid; + info.presentationTimeStamp = CMTimeMake(timeStampNs, 1000000000);; + info.duration = kCMTimeInvalid; + info.decodeTimeStamp = kCMTimeInvalid; + + CMFormatDescriptionRef formatDesc = nil; + CMVideoFormatDescriptionCreateForImageBuffer(kCFAllocatorDefault, pixelBuffer, &formatDesc); + + CMSampleBufferRef sampleBuffer = nil; + + CMSampleBufferCreateReadyWithImageBuffer(kCFAllocatorDefault, + pixelBuffer, + formatDesc, + &info, + &sampleBuffer); + + return sampleBuffer; +} + +- (UIImageOrientation)imageOrientation { + return [self imageOrientationFromDevicePosition:AVCaptureDevicePositionFront]; +} + +- (UIImageOrientation)imageOrientationFromDevicePosition:(AVCaptureDevicePosition)devicePosition { + + UIDeviceOrientation deviceOrientation = UIDevice.currentDevice.orientation; + + if (deviceOrientation == UIDeviceOrientationFaceDown || + deviceOrientation == UIDeviceOrientationFaceUp || + deviceOrientation == UIDeviceOrientationUnknown) { + deviceOrientation = [self currentUIOrientation]; + } + + switch (deviceOrientation) { + case UIDeviceOrientationPortrait: + return devicePosition == AVCaptureDevicePositionFront ? UIImageOrientationLeftMirrored + : UIImageOrientationRight; + case UIDeviceOrientationLandscapeLeft: + return devicePosition == AVCaptureDevicePositionFront ? UIImageOrientationDownMirrored + : UIImageOrientationUp; + case UIDeviceOrientationPortraitUpsideDown: + return devicePosition == AVCaptureDevicePositionFront ? UIImageOrientationRightMirrored + : UIImageOrientationLeft; + case UIDeviceOrientationLandscapeRight: + return devicePosition == AVCaptureDevicePositionFront ? UIImageOrientationUpMirrored + : UIImageOrientationDown; + case UIDeviceOrientationFaceDown: + case UIDeviceOrientationFaceUp: + case UIDeviceOrientationUnknown: + return UIImageOrientationUp; + } +} + +- (UIDeviceOrientation)currentUIOrientation { + UIDeviceOrientation (^deviceOrientation)(void) = ^UIDeviceOrientation(void) { + switch (UIApplication.sharedApplication.statusBarOrientation) { + case UIInterfaceOrientationLandscapeLeft: + return UIDeviceOrientationLandscapeRight; + case UIInterfaceOrientationLandscapeRight: + return UIDeviceOrientationLandscapeLeft; + case UIInterfaceOrientationPortraitUpsideDown: + return UIDeviceOrientationPortraitUpsideDown; + case UIInterfaceOrientationPortrait: + case UIInterfaceOrientationUnknown: + return UIDeviceOrientationPortrait; + } + }; + + if (NSThread.isMainThread) { + return deviceOrientation(); + } else { + __block UIDeviceOrientation currentOrientation = UIDeviceOrientationPortrait; + dispatch_sync(dispatch_get_main_queue(), ^{ + currentOrientation = deviceOrientation(); + }); + return currentOrientation; + } +} + +- (void)applySegmentationMask:(MLKSegmentationMask *)mask + toPixelBuffer:(CVPixelBufferRef)imageBuffer + rotation:(RTCVideoRotation)rotation{ + + CVPixelBufferRef currentBackground = NULL; + + switch (rotation) { + case RTCVideoRotation_90: + currentBackground = _backgroundBuffer; + break; + case RTCVideoRotation_0: //Right rotated screen + currentBackground = _leftRotatedBackgroundBuffer; + break; + case RTCVideoRotation_270: //Upside down screen + currentBackground = _upsideRotatedBackgroundBuffer; + break; + case RTCVideoRotation_180: //Left rotated screen + currentBackground = _rightRotatedBackgroundBuffer; + break; + } + + size_t width = CVPixelBufferGetWidth(mask.buffer); + size_t height = CVPixelBufferGetHeight(mask.buffer); + + CVPixelBufferLockBaseAddress(imageBuffer, 0); + CVPixelBufferLockBaseAddress(currentBackground, 0); + CVPixelBufferLockBaseAddress(mask.buffer, kCVPixelBufferLock_ReadOnly); + + float *maskAddress = (float *)CVPixelBufferGetBaseAddress(mask.buffer); + size_t maskBytesPerRow = CVPixelBufferGetBytesPerRow(mask.buffer); + + unsigned char *imageAddress = (unsigned char *)CVPixelBufferGetBaseAddress(imageBuffer); + size_t bytesPerRow = CVPixelBufferGetBytesPerRow(imageBuffer); + static const int kBGRABytesPerPixel = 4; + + unsigned char *backgroundImageAddress = (unsigned char *)CVPixelBufferGetBaseAddress(currentBackground); + size_t backgroundImageBytesPerRow = CVPixelBufferGetBytesPerRow(currentBackground); + + static const float kMaxColorComponentValue = 255.0f; + + for (int row = 0; row < height; ++row) { + for (int col = 0; col < width; ++col) { + int pixelOffset = col * kBGRABytesPerPixel; + int blueOffset = pixelOffset; + int greenOffset = pixelOffset + 1; + int redOffset = pixelOffset + 2; + int alphaOffset = pixelOffset + 3; + + float maskValue = maskAddress[col]; + float backgroundRegionRatio = 1.0f - maskValue; + + float originalPixelRed = imageAddress[redOffset] / kMaxColorComponentValue; + float originalPixelGreen = imageAddress[greenOffset] / kMaxColorComponentValue; + float originalPixelBlue = imageAddress[blueOffset] / kMaxColorComponentValue; + float originalPixelAlpha = imageAddress[alphaOffset] / kMaxColorComponentValue; + + float redOverlay = backgroundImageAddress[blueOffset] / kMaxColorComponentValue; + float greenOverlay = backgroundImageAddress[greenOffset] / kMaxColorComponentValue; + float blueOverlay = backgroundImageAddress[redOffset] / kMaxColorComponentValue; + float alphaOverlay = backgroundRegionRatio; + + // Calculate composite color component values. + // Derived from https://en.wikipedia.org/wiki/Alpha_compositing#Alpha_blending + float compositeAlpha = ((1.0f - alphaOverlay) * originalPixelAlpha) + alphaOverlay; + float compositeRed = 0.0f; + float compositeGreen = 0.0f; + float compositeBlue = 0.0f; + // Only perform rgb blending calculations if the output alpha is > 0. A zero-value alpha + // means none of the color channels actually matter, and would introduce division by 0. + if (fabs(compositeAlpha) > FLT_EPSILON) { + compositeRed = (((1.0f - alphaOverlay) * originalPixelAlpha * originalPixelRed) + + (alphaOverlay * redOverlay)) / + compositeAlpha; + compositeGreen = (((1.0f - alphaOverlay) * originalPixelAlpha * originalPixelGreen) + + (alphaOverlay * greenOverlay)) / + compositeAlpha; + compositeBlue = (((1.0f - alphaOverlay) * originalPixelAlpha * originalPixelBlue) + + (alphaOverlay * blueOverlay)) / + compositeAlpha; + } + + imageAddress[blueOffset] = compositeBlue * kMaxColorComponentValue; + imageAddress[greenOffset] = compositeGreen * kMaxColorComponentValue; + imageAddress[redOffset] = compositeRed * kMaxColorComponentValue; + imageAddress[alphaOffset] = compositeAlpha * kMaxColorComponentValue; + } + imageAddress += bytesPerRow / sizeof(unsigned char); + backgroundImageAddress += backgroundImageBytesPerRow / sizeof(unsigned char); + maskAddress += maskBytesPerRow / sizeof(float); + } + + CVPixelBufferUnlockBaseAddress(imageBuffer, 0); + CVPixelBufferUnlockBaseAddress(currentBackground, 0); + CVPixelBufferUnlockBaseAddress(mask.buffer, kCVPixelBufferLock_ReadOnly); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/ios/RCTWebRTC/WebRTCModule+RTCMediaStream.m b/ios/RCTWebRTC/WebRTCModule+RTCMediaStream.m index 654bbaa44..f1049e635 100644 --- a/ios/RCTWebRTC/WebRTCModule+RTCMediaStream.m +++ b/ios/RCTWebRTC/WebRTCModule+RTCMediaStream.m @@ -45,10 +45,26 @@ - (RTCVideoTrack *)createVideoTrack:(NSDictionary *)constraints { RTCVideoTrack *videoTrack = [self.peerConnectionFactory videoTrackWithSource:videoSource trackId:trackUUID]; #if !TARGET_IPHONE_SIMULATOR - RTCCameraVideoCapturer *videoCapturer = [[RTCCameraVideoCapturer alloc] initWithDelegate:videoSource]; + NSDictionary *videoContraints = constraints[@"video"]; + RTCCameraVideoCapturer *videoCapturer; + + NSLog(@"SIDAL video constraint %@", videoContraints); + + // If virtual backround is enabled, use video source interceptor before video source + if (videoContraints[@"vb"]) { + self.videoSourceInterceptor = [[VideoSourceInterceptor alloc]initWithVideoSource:videoSource]; + videoCapturer = [[RTCCameraVideoCapturer alloc] initWithDelegate:self.videoSourceInterceptor]; + } + else { + videoCapturer = [[RTCCameraVideoCapturer alloc] initWithDelegate:videoSource]; + } + +// self.videoSourceInterceptor = [[VideoSourceInterceptor alloc]initWithVideoSource:videoSource]; +// videoCapturer = [[RTCCameraVideoCapturer alloc] initWithDelegate:self.videoSourceInterceptor]; + VideoCaptureController *videoCaptureController = [[VideoCaptureController alloc] initWithCapturer:videoCapturer - andConstraints:constraints[@"video"]]; + andConstraints:videoContraints]; videoTrack.captureController = videoCaptureController; [videoCaptureController startCapture]; #endif @@ -114,6 +130,9 @@ - (RTCVideoTrack *)createScreenCaptureVideoTrack { RCT_EXPORT_METHOD(getUserMedia:(NSDictionary *)constraints successCallback:(RCTResponseSenderBlock)successCallback errorCallback:(RCTResponseSenderBlock)errorCallback) { + + NSLog(@"SIDAL video constraint in get user media %@", constraints); + RTCAudioTrack *audioTrack = nil; RTCVideoTrack *videoTrack = nil; @@ -278,6 +297,9 @@ - (RTCVideoTrack *)createScreenCaptureVideoTrack { track.isEnabled = NO; [track.captureController stopCapture]; [self.localTracks removeObjectForKey:trackID]; + if([track.kind isEqualToString:kRTCMediaStreamTrackKindVideo]) { + self.videoSourceInterceptor = nil; + } } } diff --git a/ios/RCTWebRTC/WebRTCModule+RTCPeerConnection.m b/ios/RCTWebRTC/WebRTCModule+RTCPeerConnection.m index 822f58b59..8ac7c63da 100644 --- a/ios/RCTWebRTC/WebRTCModule+RTCPeerConnection.m +++ b/ios/RCTWebRTC/WebRTCModule+RTCPeerConnection.m @@ -444,8 +444,6 @@ - (void)appendValue:(NSObject *)statisticsValue toString:(NSMutableString *)s { NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; [s appendString:jsonString]; } - } else if ([statisticsValue isKindOfClass:[NSNumber class]]) { - [s appendString:[NSString stringWithFormat:@"%@", statisticsValue]]; } else { [s appendString:@"\""]; [s appendString:[NSString stringWithFormat:@"%@", statisticsValue]]; diff --git a/ios/RCTWebRTC/WebRTCModule.h b/ios/RCTWebRTC/WebRTCModule.h index dee0d71c6..797d8c8e0 100644 --- a/ios/RCTWebRTC/WebRTCModule.h +++ b/ios/RCTWebRTC/WebRTCModule.h @@ -19,6 +19,7 @@ #import #import #import +#import "VideoSourceInterceptor.h" static NSString *const kEventPeerConnectionSignalingStateChanged = @"peerConnectionSignalingStateChanged"; static NSString *const kEventPeerConnectionStateChanged = @"peerConnectionStateChanged"; @@ -42,6 +43,7 @@ static NSString *const kEventMediaStreamTrackMuteChanged = @"mediaStreamTrackMut @property (nonatomic, strong) NSMutableDictionary *peerConnections; @property (nonatomic, strong) NSMutableDictionary *localStreams; @property (nonatomic, strong) NSMutableDictionary *localTracks; +@property (nonatomic, strong) VideoSourceInterceptor *videoSourceInterceptor; - (instancetype)initWithEncoderFactory:(id)encoderFactory decoderFactory:(id)decoderFactory; diff --git a/package.json b/package.json index a2ca75919..3c1d5a2f3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-webrtc", - "version": "1.100.1", + "version": "1.100.3", "repository": { "type": "git", "url": "git+https://github.com/react-native-webrtc/react-native-webrtc.git" diff --git a/src/RTCUtil.ts b/src/RTCUtil.ts index 1d1c6d7eb..0568340fe 100644 --- a/src/RTCUtil.ts +++ b/src/RTCUtil.ts @@ -200,5 +200,11 @@ export function normalizeConstraints(constraints) { } } + if(constraints['video'] && constraints['video'].hasOwnProperty('vb')) { + if(c['video']) { + c['video'].vb = true; + } + } + return c; } diff --git a/src/getUserMedia.ts b/src/getUserMedia.ts index 3c32cf804..ebfb2b0d9 100644 --- a/src/getUserMedia.ts +++ b/src/getUserMedia.ts @@ -14,7 +14,6 @@ interface Constraints { } export default function getUserMedia(constraints: Constraints = {}) { - console.log("getuserMedia : ", constraints) // According to // https://www.w3.org/TR/mediacapture-streams/#dom-mediadevices-getusermedia, // the constraints argument is a dictionary of type MediaStreamConstraints. @@ -31,6 +30,7 @@ export default function getUserMedia(constraints: Constraints = {}) { // Normalize constraints. constraints = RTCUtil.normalizeConstraints(constraints); + console.log("sidal 44444 constraints: ", constraints); // Request required permissions const reqPermissions: Array> = []; From 9809afce61fbb3841eea832ef31a49fff499455e Mon Sep 17 00:00:00 2001 From: "TURKCELL\\TCSOTHAN" Date: Fri, 26 Aug 2022 16:34:44 +0300 Subject: [PATCH 03/10] virtual backgorund image used from project instead of url --- ios/RCTWebRTC/VideoSourceInterceptor.m | 37 ++++++++++---------------- 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/ios/RCTWebRTC/VideoSourceInterceptor.m b/ios/RCTWebRTC/VideoSourceInterceptor.m index 8174471bf..2a7f80832 100644 --- a/ios/RCTWebRTC/VideoSourceInterceptor.m +++ b/ios/RCTWebRTC/VideoSourceInterceptor.m @@ -29,15 +29,15 @@ @interface VideoSourceInterceptor () @property (nonatomic) CVPixelBufferRef leftRotatedBackgroundBuffer; @property (nonatomic) CVPixelBufferRef upsideRotatedBackgroundBuffer; +@property (nonatomic) UIImage *backgroundImage; +@property (nonatomic) UIImage *rightRotatedBackgroundImage; +@property (nonatomic) UIImage *leftRotatedBackgroundImage; +@property (nonatomic) UIImage *upsideDownBackgroundImage; + @end @implementation VideoSourceInterceptor -NSString * const portraitBackgroundImageUrl = @"https://i.ibb.co/5RMCH5G/portrait.jpg"; -NSString * const rightRotatedBackgroundImageUrl = @"https://i.ibb.co/YNsR7St/rotated-Right.jpg"; -NSString * const leftRotatedBackgroundImageUrl = @"https://i.ibb.co/cwwSKFn/rotated-Left.jpg"; -NSString * const upsideBackgroundImageUrl = @"https://i.ibb.co/mcSJZQk/upside.jpg"; - - (instancetype)initWithVideoSource: (RTCVideoSource*) videoSource { if (self = [super init]) { _videoSource = videoSource; @@ -48,28 +48,19 @@ - (instancetype)initWithVideoSource: (RTCVideoSource*) videoSource { self.segmenter = [MLKSegmenter segmenterWithOptions:options]; - _backgroundBuffer = [self pixelBufferFromImageUrl:portraitBackgroundImageUrl]; - _rightRotatedBackgroundBuffer = [self pixelBufferFromImageUrl:rightRotatedBackgroundImageUrl]; - _leftRotatedBackgroundBuffer = [self pixelBufferFromImageUrl:leftRotatedBackgroundImageUrl]; - _upsideRotatedBackgroundBuffer = [self pixelBufferFromImageUrl:upsideBackgroundImageUrl]; + _backgroundImage = [UIImage imageNamed:@"portraitBackground"]; + _rightRotatedBackgroundImage = [UIImage imageNamed:@"rightRotatedBackground"]; + _leftRotatedBackgroundImage = [UIImage imageNamed:@"leftRotatedBackground"]; + _upsideDownBackgroundImage = [UIImage imageNamed:@"upsideDownBackground"]; + + _backgroundBuffer = [self pixelBufferFromCGImage:_backgroundImage.CGImage]; + _rightRotatedBackgroundBuffer = [self pixelBufferFromCGImage:_rightRotatedBackgroundImage.CGImage]; + _leftRotatedBackgroundBuffer = [self pixelBufferFromCGImage:_leftRotatedBackgroundImage.CGImage]; + _upsideRotatedBackgroundBuffer = [self pixelBufferFromCGImage:_upsideDownBackgroundImage.CGImage]; } return self; } -- (CVPixelBufferRef) pixelBufferFromImageUrl: (NSString *) imageURL -{ - NSURL *url = [NSURL URLWithString:imageURL]; - CGDataProviderRef dataProvider = CGDataProviderCreateWithURL((CFURLRef)url); - CGImageRef backgroundRef = CGImageCreateWithJPEGDataProvider(dataProvider, NULL, true, kCGRenderingIntentDefault); - - CVPixelBufferRef resultBuffer = NULL; - resultBuffer = [self pixelBufferFromCGImage:backgroundRef]; - - CGDataProviderRelease(dataProvider); - - return resultBuffer; -} - - (CVPixelBufferRef) pixelBufferFromCGImage: (CGImageRef) image { NSDictionary *options = @{ From f3d55e1122b822ba2b7717d1b94da919b8b839d1 Mon Sep 17 00:00:00 2001 From: "TURKCELL\\TCSOTHAN" Date: Fri, 26 Aug 2022 16:48:42 +0300 Subject: [PATCH 04/10] prevent pink screen on vb --- ios/RCTWebRTC/VideoCaptureController.m | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/ios/RCTWebRTC/VideoCaptureController.m b/ios/RCTWebRTC/VideoCaptureController.m index 8ecf374f1..1ebed9daf 100644 --- a/ios/RCTWebRTC/VideoCaptureController.m +++ b/ios/RCTWebRTC/VideoCaptureController.m @@ -15,6 +15,7 @@ @interface VideoCaptureController () @property (nonatomic, assign) int width; @property (nonatomic, assign) int height; @property (nonatomic, assign) int frameRate; +@property (nonatomic, assign) BOOL vb; @end @@ -34,6 +35,9 @@ - (instancetype)initWithCapturer:(RTCCameraVideoCapturer *)capturer self.width = [constraints[@"width"] intValue]; self.height = [constraints[@"height"] intValue]; self.frameRate = [constraints[@"frameRate"] intValue]; + if(constraints[@"vb"]) { + self.vb = YES; + } id facingMode = constraints[@"facingMode"]; @@ -96,6 +100,17 @@ - (void)startCapture { dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); __weak VideoCaptureController *weakSelf = self; + + if (self.vb) { + // Selfie Segmentator library requires kCVPixelFormatType_32BGRA format + for (AVCaptureOutput *output in _capturer.captureSession.outputs) { + RCTLog(@"Changing capturer output to %@", ((AVCaptureVideoDataOutput*)output).videoSettings); + if([output isKindOfClass:AVCaptureVideoDataOutput.class]) { + ((AVCaptureVideoDataOutput*)output).videoSettings = @{(NSString *)kCVPixelBufferPixelFormatTypeKey: [NSNumber numberWithUnsignedInt:kCVPixelFormatType_32BGRA]}; + } + } + } + [self.capturer startCaptureWithDevice:self.device format:format fps:self.frameRate completionHandler:^(NSError *err) { if (err) { RCTLogError(@"[VideoCaptureController] Error starting capture: %@", err); From 5e77cafdb3b73a6086379ff40bbe2a7c0d00abf0 Mon Sep 17 00:00:00 2001 From: "TURKCELL\\TCSOTHAN" Date: Thu, 8 Sep 2022 11:51:23 +0300 Subject: [PATCH 05/10] changed throttle durations reworded logs --- ios/RCTWebRTC/VideoCaptureController.m | 4 ++-- ios/RCTWebRTC/WebRTCModule+RTCMediaStream.m | 4 ++-- src/getUserMedia.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ios/RCTWebRTC/VideoCaptureController.m b/ios/RCTWebRTC/VideoCaptureController.m index 1ebed9daf..9cd74af9a 100644 --- a/ios/RCTWebRTC/VideoCaptureController.m +++ b/ios/RCTWebRTC/VideoCaptureController.m @@ -242,8 +242,8 @@ - (void)throttleFrameRateForDevice:(AVCaptureDevice *)device { return; } - device.activeVideoMinFrameDuration = CMTimeMake(1, 20); - device.activeVideoMaxFrameDuration = CMTimeMake(1, 15); + device.activeVideoMinFrameDuration = CMTimeMake(1, 15); + device.activeVideoMaxFrameDuration = CMTimeMake(1, 12); [device unlockForConfiguration]; } diff --git a/ios/RCTWebRTC/WebRTCModule+RTCMediaStream.m b/ios/RCTWebRTC/WebRTCModule+RTCMediaStream.m index f1049e635..5e2bcc567 100644 --- a/ios/RCTWebRTC/WebRTCModule+RTCMediaStream.m +++ b/ios/RCTWebRTC/WebRTCModule+RTCMediaStream.m @@ -48,7 +48,7 @@ - (RTCVideoTrack *)createVideoTrack:(NSDictionary *)constraints { NSDictionary *videoContraints = constraints[@"video"]; RTCCameraVideoCapturer *videoCapturer; - NSLog(@"SIDAL video constraint %@", videoContraints); + NSLog(@"Video constraint in create video track: %@", videoContraints); // If virtual backround is enabled, use video source interceptor before video source if (videoContraints[@"vb"]) { @@ -131,7 +131,7 @@ - (RTCVideoTrack *)createScreenCaptureVideoTrack { successCallback:(RCTResponseSenderBlock)successCallback errorCallback:(RCTResponseSenderBlock)errorCallback) { - NSLog(@"SIDAL video constraint in get user media %@", constraints); + NSLog(@"Video constraint in RTCMediaStream get user media %@", constraints); RTCAudioTrack *audioTrack = nil; RTCVideoTrack *videoTrack = nil; diff --git a/src/getUserMedia.ts b/src/getUserMedia.ts index ebfb2b0d9..dd9511aa4 100644 --- a/src/getUserMedia.ts +++ b/src/getUserMedia.ts @@ -30,7 +30,7 @@ export default function getUserMedia(constraints: Constraints = {}) { // Normalize constraints. constraints = RTCUtil.normalizeConstraints(constraints); - console.log("sidal 44444 constraints: ", constraints); + console.log("Constraints in getUserMedia: ", constraints); // Request required permissions const reqPermissions: Array> = []; From d2359473880fcdb4665cd86eb10ff3e2dc81f17f Mon Sep 17 00:00:00 2001 From: "TURKCELL\\TCSOTHAN" Date: Thu, 8 Sep 2022 17:29:43 +0300 Subject: [PATCH 06/10] added framework search paths --- ios/RCTWebRTC.xcodeproj/project.pbxproj | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/ios/RCTWebRTC.xcodeproj/project.pbxproj b/ios/RCTWebRTC.xcodeproj/project.pbxproj index 0a4611097..87ebfbe73 100644 --- a/ios/RCTWebRTC.xcodeproj/project.pbxproj +++ b/ios/RCTWebRTC.xcodeproj/project.pbxproj @@ -307,7 +307,15 @@ 35A222291CB493700015FD5C /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - FRAMEWORK_SEARCH_PATHS = "$(PROJECT_DIR)/../apple/"; + FRAMEWORK_SEARCH_PATHS = ( + "$(PROJECT_DIR)/../apple/", + "\"${PODS_ROOT}/MLKitSegmentationSelfie/Frameworks\"", + "\"${PODS_ROOT}/MLKitSegmentationCommon/Frameworks\"", + "\"${PODS_ROOT}/MLKitCommon/Frameworks\"", + "\"${PODS_ROOT}/MLImage/Frameworks\"", + "\"${PODS_ROOT}/MLKitVision/Frameworks\"", + "\"${PODS_ROOT}/MLKitXenoCommon/Frameworks\"", + ); LIBRARY_SEARCH_PATHS = "$(inherited)"; OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -318,7 +326,15 @@ 35A2222A1CB493700015FD5C /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - FRAMEWORK_SEARCH_PATHS = "$(PROJECT_DIR)/../apple/"; + FRAMEWORK_SEARCH_PATHS = ( + "$(PROJECT_DIR)/../apple/", + "\"${PODS_ROOT}/MLKitSegmentationSelfie/Frameworks\"", + "\"${PODS_ROOT}/MLKitSegmentationCommon/Frameworks\"", + "\"${PODS_ROOT}/MLKitCommon/Frameworks\"", + "\"${PODS_ROOT}/MLImage/Frameworks\"", + "\"${PODS_ROOT}/MLKitVision/Frameworks\"", + "\"${PODS_ROOT}/MLKitXenoCommon/Frameworks\"", + ); LIBRARY_SEARCH_PATHS = "$(inherited)"; OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = "$(TARGET_NAME)"; From 0381210b578d55a68084230bbafed79f78c21770 Mon Sep 17 00:00:00 2001 From: "TURKCELL\\TCSOTHAN" Date: Thu, 22 Sep 2022 12:03:34 +0300 Subject: [PATCH 07/10] apply throttle only for vb case --- ios/RCTWebRTC/VideoCaptureController.m | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/ios/RCTWebRTC/VideoCaptureController.m b/ios/RCTWebRTC/VideoCaptureController.m index 9cd74af9a..a53483186 100644 --- a/ios/RCTWebRTC/VideoCaptureController.m +++ b/ios/RCTWebRTC/VideoCaptureController.m @@ -102,7 +102,7 @@ - (void)startCapture { __weak VideoCaptureController *weakSelf = self; if (self.vb) { - // Selfie Segmentator library requires kCVPixelFormatType_32BGRA format + // ML Kit library requires kCVPixelFormatType_32BGRA format for (AVCaptureOutput *output in _capturer.captureSession.outputs) { RCTLog(@"Changing capturer output to %@", ((AVCaptureVideoDataOutput*)output).videoSettings); if([output isKindOfClass:AVCaptureVideoDataOutput.class]) { @@ -242,8 +242,13 @@ - (void)throttleFrameRateForDevice:(AVCaptureDevice *)device { return; } - device.activeVideoMinFrameDuration = CMTimeMake(1, 15); - device.activeVideoMaxFrameDuration = CMTimeMake(1, 12); + if (self.vb) { + device.activeVideoMinFrameDuration = CMTimeMake(1, 15); + device.activeVideoMaxFrameDuration = CMTimeMake(1, 12); + } else { + device.activeVideoMinFrameDuration = CMTimeMake(1, 20); + device.activeVideoMaxFrameDuration = CMTimeMake(1, 15); + } [device unlockForConfiguration]; } From cc706f3957966b31cdf2c06192aedd1d44d7b495 Mon Sep 17 00:00:00 2001 From: "TURKCELL\\TCSOTHAN" Date: Wed, 19 Oct 2022 13:21:16 +0300 Subject: [PATCH 08/10] added android vb feature --- android/build.gradle | 10 + .../WebRTCModule/CameraCaptureController.java | 2 +- .../oney/WebRTCModule/GetUserMediaImpl.java | 12 +- .../VirtualBackgroundVideoProcessor.java | 152 ++++++++++++++ .../java/com/oney/WebRTCModule/YuvFrame.java | 191 ++++++++++++++++++ .../main/res/drawable/portrait_background.jpg | Bin 0 -> 352411 bytes 6 files changed, 363 insertions(+), 4 deletions(-) create mode 100644 android/src/main/java/com/oney/WebRTCModule/VirtualBackgroundVideoProcessor.java create mode 100644 android/src/main/java/com/oney/WebRTCModule/YuvFrame.java create mode 100644 android/src/main/res/drawable/portrait_background.jpg diff --git a/android/build.gradle b/android/build.gradle index 7180a6c2b..052cfdaa1 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -4,6 +4,15 @@ def safeExtGet(prop, fallback) { rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback } +allprojects { + repositories { + google() + jcenter() + maven { url 'https://www.jitpack.io' } + mavenCentral() + } +} + android { compileSdkVersion safeExtGet('compileSdkVersion', 23) buildToolsVersion safeExtGet('buildToolsVersion', "23.0.1") @@ -28,5 +37,6 @@ android { dependencies { implementation 'com.facebook.react:react-native:+' + implementation 'com.google.mlkit:segmentation-selfie:16.0.0-beta4' api fileTree(dir: 'libs', include: ['*.jar']) } diff --git a/android/src/main/java/com/oney/WebRTCModule/CameraCaptureController.java b/android/src/main/java/com/oney/WebRTCModule/CameraCaptureController.java index bcef7b1f6..86c7acdd4 100644 --- a/android/src/main/java/com/oney/WebRTCModule/CameraCaptureController.java +++ b/android/src/main/java/com/oney/WebRTCModule/CameraCaptureController.java @@ -35,7 +35,7 @@ public CameraCaptureController(CameraEnumerator cameraEnumerator, ReadableMap co super( constraints.getInt("width"), constraints.getInt("height"), - constraints.getInt("frameRate")); + constraints.hasKey("vb") ? 5 : constraints.getInt("frameRate")); this.cameraEnumerator = cameraEnumerator; this.constraints = constraints; diff --git a/android/src/main/java/com/oney/WebRTCModule/GetUserMediaImpl.java b/android/src/main/java/com/oney/WebRTCModule/GetUserMediaImpl.java index 5097b5561..a52819f88 100644 --- a/android/src/main/java/com/oney/WebRTCModule/GetUserMediaImpl.java +++ b/android/src/main/java/com/oney/WebRTCModule/GetUserMediaImpl.java @@ -195,11 +195,12 @@ void getUserMedia( Log.d(TAG, "getUserMedia(video): " + videoConstraintsMap); + //CameraCaptureController constructor sets frame rate to 5 if virtual background is on. CameraCaptureController cameraCaptureController = new CameraCaptureController( cameraEnumerator, videoConstraintsMap); - videoTrack = createVideoTrack(cameraCaptureController); + videoTrack = createVideoTrack(cameraCaptureController, videoConstraintsMap.hasKey("vb")); } if (audioTrack == null && videoTrack == null) { @@ -355,10 +356,10 @@ private VideoTrack createScreenTrack() { int height = displayMetrics.heightPixels; ScreenCaptureController screenCaptureController = new ScreenCaptureController(reactContext.getCurrentActivity(), width, height, mediaProjectionPermissionResultData); - return createVideoTrack(screenCaptureController); + return createVideoTrack(screenCaptureController, false); } - private VideoTrack createVideoTrack(AbstractVideoCaptureController videoCaptureController) { + private VideoTrack createVideoTrack(AbstractVideoCaptureController videoCaptureController, Boolean vb) { videoCaptureController.initializeVideoCapturer(); VideoCapturer videoCapturer = videoCaptureController.videoCapturer; @@ -379,6 +380,11 @@ private VideoTrack createVideoTrack(AbstractVideoCaptureController videoCaptureC VideoSource videoSource = pcFactory.createVideoSource(videoCapturer.isScreencast()); videoCapturer.initialize(surfaceTextureHelper, reactContext, videoSource.getCapturerObserver()); + if(vb) { + VideoProcessor p = new VirtualBackgroundVideoProcessor(reactContext, surfaceTextureHelper); + videoSource.setVideoProcessor(p); + } + String id = UUID.randomUUID().toString(); VideoTrack track = pcFactory.createVideoTrack(id, videoSource); diff --git a/android/src/main/java/com/oney/WebRTCModule/VirtualBackgroundVideoProcessor.java b/android/src/main/java/com/oney/WebRTCModule/VirtualBackgroundVideoProcessor.java new file mode 100644 index 000000000..fa42c43c0 --- /dev/null +++ b/android/src/main/java/com/oney/WebRTCModule/VirtualBackgroundVideoProcessor.java @@ -0,0 +1,152 @@ +package com.oney.WebRTCModule; + +import static android.graphics.Color.argb; +import static android.graphics.PorterDuff.Mode.DST_OVER; +import static android.graphics.PorterDuff.Mode.SRC_IN; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.PorterDuffXfermode; +import android.opengl.GLES20; +import android.opengl.GLUtils; + +import androidx.annotation.Nullable; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.google.android.gms.tasks.OnSuccessListener; +import com.google.android.gms.tasks.Task; +import com.google.mlkit.vision.common.InputImage; +import com.google.mlkit.vision.segmentation.Segmentation; +import com.google.mlkit.vision.segmentation.SegmentationMask; +import com.google.mlkit.vision.segmentation.Segmenter; +import com.google.mlkit.vision.segmentation.selfie.SelfieSegmenterOptions; + +import org.webrtc.SurfaceTextureHelper; +import org.webrtc.TextureBufferImpl; +import org.webrtc.VideoFrame; +import org.webrtc.VideoProcessor; +import org.webrtc.VideoSink; +import org.webrtc.YuvConverter; + +public class VirtualBackgroundVideoProcessor implements VideoProcessor { + + private VideoSink target; + private final SurfaceTextureHelper surfaceTextureHelper; + final YuvConverter yuvConverter = new YuvConverter(); + + final Bitmap backgroundImage; + final Bitmap scaled; + + final SelfieSegmenterOptions options = + new SelfieSegmenterOptions.Builder() + .setDetectorMode(SelfieSegmenterOptions.STREAM_MODE) + .build(); + final Segmenter segmenter = Segmentation.getClient(options); + + public VirtualBackgroundVideoProcessor(ReactApplicationContext context, SurfaceTextureHelper surfaceTextureHelper) { + super(); + + this.surfaceTextureHelper = surfaceTextureHelper; + + backgroundImage = BitmapFactory.decodeResource(context.getResources(), R.drawable.portrait_background); + scaled = Bitmap.createScaledBitmap(backgroundImage, 640, 640, false ); + } + + @Override + public void setSink(@Nullable VideoSink videoSink) { + target = videoSink; + } + + @Override + public void onCapturerStarted(boolean b) { + + } + + @Override + public void onCapturerStopped() { + + } + + @Override + public void onFrameCaptured(VideoFrame videoFrame) { + + YuvFrame yuvFrame = new YuvFrame(videoFrame); + Bitmap inputFrameBitmap = yuvFrame.getBitmap(); + + InputImage image = InputImage.fromBitmap(inputFrameBitmap, 0); + + Task result = + segmenter.process(image) + .addOnSuccessListener( + new OnSuccessListener() { + @Override + public void onSuccess(SegmentationMask mask) { + + mask.getBuffer().rewind(); + int[] arr = maskColorsFromByteBuffer(mask); + Bitmap segmentedBitmap = Bitmap.createBitmap( + arr, mask.getWidth(), mask.getHeight(), Bitmap.Config.ARGB_8888 + ); + arr = null; + + Bitmap segmentedBitmapMutable = segmentedBitmap.copy(Bitmap.Config.ARGB_8888, true); + segmentedBitmap.recycle(); + Canvas canvas = new Canvas(segmentedBitmapMutable); + + Paint paint = new Paint(); + paint.setXfermode(new PorterDuffXfermode(SRC_IN)); + canvas.drawBitmap(scaled, 0, 0, paint); + paint.setXfermode(new PorterDuffXfermode(DST_OVER)); + canvas.drawBitmap(inputFrameBitmap, 0, 0, paint); + + surfaceTextureHelper.getHandler().post(new Runnable() { + @Override + public void run() { + + GLES20.glActiveTexture(GLES20.GL_TEXTURE0); + TextureBufferImpl buffer = new TextureBufferImpl(segmentedBitmapMutable.getWidth(), + segmentedBitmapMutable.getHeight(), VideoFrame.TextureBuffer.Type.RGB, + GLES20.GL_TEXTURE0, new Matrix(), surfaceTextureHelper.getHandler(), yuvConverter, null); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE0); + + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST); + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST); + GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, segmentedBitmapMutable, 0); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0); + + VideoFrame.I420Buffer i420Buf = yuvConverter.convert(buffer); + VideoFrame out = new VideoFrame(i420Buf, 180, videoFrame.getTimestampNs()); + + buffer.release(); + yuvFrame.dispose(); + + target.onFrame(out); + out.release(); + } + }); + + } + }); + } + + private int[] maskColorsFromByteBuffer(SegmentationMask mask) { + int[] colors = new int[mask.getHeight() * mask.getWidth()]; + for (int i = 0; i < mask.getHeight() * mask.getWidth(); i++) { + float backgroundLikelihood = 1 - mask.getBuffer().getFloat(); + if (backgroundLikelihood > 0.9) { + colors[i] = argb(255, 255, 0, 255); + } else if (backgroundLikelihood > 0.2) { + // Linear interpolation to make sure when backgroundLikelihood is 0.2, the alpha is 0 and + // when backgroundLikelihood is 0.9, the alpha is 128. + // +0.5 to round the float value to the nearest int. + double d = 182.9 * backgroundLikelihood - 36.6 + 0.5; + int alpha = (int) d; + colors[i] = argb(alpha, 255, 0, 255); + } + } + return colors; + } +} diff --git a/android/src/main/java/com/oney/WebRTCModule/YuvFrame.java b/android/src/main/java/com/oney/WebRTCModule/YuvFrame.java new file mode 100644 index 000000000..bf1c0b73a --- /dev/null +++ b/android/src/main/java/com/oney/WebRTCModule/YuvFrame.java @@ -0,0 +1,191 @@ +package com.oney.WebRTCModule; + +import android.graphics.Bitmap; +import android.graphics.Matrix; + +import org.webrtc.VideoFrame; + +import java.nio.ByteBuffer; + +public class YuvFrame { + public int width; + public int height; + public byte[] nv21Buffer; + public int rotationDegree; + public long timestamp; + + private final Object planeLock = new Object(); + + public YuvFrame(final VideoFrame videoFrame) { + fromVideoFrame(videoFrame, System.nanoTime()); + } + + public void fromVideoFrame(final VideoFrame videoFrame, final long timestamp) { + if (videoFrame == null) { + return; + } + + synchronized (planeLock) { + try { + // Save timestamp + this.timestamp = timestamp; + + // Copy rotation information + rotationDegree = videoFrame.getRotation(); // Just save rotation info for now, doing actual rotation can wait until per-pixel processing. + + // Copy the pixel data, processing as requested. + copyPlanes(videoFrame.getBuffer()); + } catch (Throwable t) { + dispose(); + } + } + } + + public void dispose() { + nv21Buffer = null; + } + + private void copyPlanes( final VideoFrame.Buffer videoFrameBuffer ) + { + VideoFrame.I420Buffer i420Buffer = null; + + if ( videoFrameBuffer != null ) + { + i420Buffer = videoFrameBuffer.toI420(); + } + + if ( i420Buffer == null ) + { + return; + } + + synchronized ( planeLock ) + { + // Set the width and height of the frame. + width = i420Buffer.getWidth(); + height = i420Buffer.getHeight(); + + // Calculate sizes needed to convert to NV21 buffer format + final int size = width * height; + final int chromaStride = width; + final int chromaWidth = ( width + 1 ) / 2; + final int chromaHeight = ( height + 1 ) / 2; + final int nv21Size = size + chromaStride * chromaHeight; + + if ( nv21Buffer == null || nv21Buffer.length != nv21Size ) + { + nv21Buffer = new byte[nv21Size]; + } + + final ByteBuffer yPlane = i420Buffer.getDataY(); + final ByteBuffer uPlane = i420Buffer.getDataU(); + final ByteBuffer vPlane = i420Buffer.getDataV(); + final int yStride = i420Buffer.getStrideY(); + final int uStride = i420Buffer.getStrideU(); + final int vStride = i420Buffer.getStrideV(); + + // Populate a buffer in NV21 format because that's what the converter wants + for ( int y = 0; y < height; y++ ) + { + for ( int x = 0; x < width; x++ ) + { + nv21Buffer[y * width + x] = yPlane.get( y * yStride + x ); + } + } + + for ( int y = 0; y < chromaHeight; y++ ) + { + for ( int x = 0; x < chromaWidth; x++ ) + { + // Swapping U and V values here because it makes the image the right color + + // Store V + nv21Buffer[size + y * chromaStride + 2 * x + 1] = uPlane.get( y * uStride + x ); + + // Store U + nv21Buffer[size + y * chromaStride + 2 * x] = vPlane.get( y * vStride + x ); + } + } + } + i420Buffer.release(); + } + + public Bitmap getBitmap() + { + if ( nv21Buffer == null ) + { + return null; + } + + // Calculate the size of the frame + final int size = width * height; + + // Allocate an array to hold the ARGB pixel data + int[] argb = new int[size]; + + // Use the converter (based on WebRTC source) to change to ARGB format + YUV_NV21_TO_RGB(argb, nv21Buffer, width, height); + + // Construct a Bitmap based on the new pixel data + Bitmap bitmap = Bitmap.createBitmap( argb, width, height, Bitmap.Config.ARGB_8888 ); + argb = null; + + // If necessary, generate a rotated version of the Bitmap + if ( rotationDegree == 90 || rotationDegree == -270 ) + { + final Matrix m = new Matrix(); + m.postRotate( 90 ); + + return Bitmap.createBitmap( bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), m, true ); + } + else if ( rotationDegree == 180 || rotationDegree == -180 ) + { + final Matrix m = new Matrix(); + m.postRotate( 180 ); + + return Bitmap.createBitmap( bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), m, true ); + } + else if ( rotationDegree == 270 || rotationDegree == -90 ) + { + final Matrix m = new Matrix(); + m.postRotate( 270 ); + + return Bitmap.createBitmap( bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), m, true ); + } + else + { + // Don't rotate, just return the Bitmap + return bitmap; + } + } + + public static void YUV_NV21_TO_RGB(int[] argb, byte[] yuv, int width, int height) { + final int frameSize = width * height; + + final int ii = 0; + final int ij = 0; + final int di = +1; + final int dj = +1; + + int a = 0; + for (int i = 0, ci = ii; i < height; ++i, ci += di) { + for (int j = 0, cj = ij; j < width; ++j, cj += dj) { + int y = (0xff & ((int) yuv[ci * width + cj])); + int v = (0xff & ((int) yuv[frameSize + (ci >> 1) * width + (cj & ~1) + 0])); + int u = (0xff & ((int) yuv[frameSize + (ci >> 1) * width + (cj & ~1) + 1])); + y = y < 16 ? 16 : y; + + int r = (int) (1.164f * (y - 16) + 1.596f * (v - 128)); + int g = (int) (1.164f * (y - 16) - 0.813f * (v - 128) - 0.391f * (u - 128)); + int b = (int) (1.164f * (y - 16) + 2.018f * (u - 128)); + + r = r < 0 ? 0 : (r > 255 ? 255 : r); + g = g < 0 ? 0 : (g > 255 ? 255 : g); + b = b < 0 ? 0 : (b > 255 ? 255 : b); + + argb[a++] = 0xff000000 | (r << 16) | (g << 8) | b; + } + } + } +} + diff --git a/android/src/main/res/drawable/portrait_background.jpg b/android/src/main/res/drawable/portrait_background.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fc9233db363061bd7127384f46c30d542264f6a3 GIT binary patch literal 352411 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 z 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