Skip to content

Commit

Permalink
Allow image scaling on the mjpeg stream (#138)
Browse files Browse the repository at this point in the history
  • Loading branch information
dmissmann authored and mykola-mokhnach committed Feb 5, 2019
1 parent a8bac49 commit c83f796
Show file tree
Hide file tree
Showing 9 changed files with 331 additions and 1 deletion.
16 changes: 16 additions & 0 deletions WebDriverAgent.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@

/* Begin PBXBuildFile section */
1FC3B2E32121ECF600B61EE0 /* FBApplicationProcessProxyTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FC3B2E12121EC8C00B61EE0 /* FBApplicationProcessProxyTests.m */; };
63CCF91221ECE4C700E94ABD /* FBImageIOScaler.h in Headers */ = {isa = PBXBuildFile; fileRef = 63CCF91021ECE4C700E94ABD /* FBImageIOScaler.h */; };
63CCF91321ECE4C700E94ABD /* FBImageIOScaler.m in Sources */ = {isa = PBXBuildFile; fileRef = 63CCF91121ECE4C700E94ABD /* FBImageIOScaler.m */; };
63FD950221F9D06100A3E356 /* FBImageIOScalerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 631B523421F6174300625362 /* FBImageIOScalerTests.m */; };
63FD950321F9D06100A3E356 /* FBImageIOScalerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 631B523421F6174300625362 /* FBImageIOScalerTests.m */; };
63FD950421F9D06200A3E356 /* FBImageIOScalerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 631B523421F6174300625362 /* FBImageIOScalerTests.m */; };
710181F8211DF584002FD3A8 /* CocoaAsyncSocket.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 710181F7211DF584002FD3A8 /* CocoaAsyncSocket.framework */; };
71018201211DF62C002FD3A8 /* libxml2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 71018200211DF62C002FD3A8 /* libxml2.tbd */; };
7101820D211E026B002FD3A8 /* libAccessibility.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 7101820C211E026B002FD3A8 /* libAccessibility.tbd */; };
Expand Down Expand Up @@ -453,6 +458,9 @@
1BA7DD8C206D694B007C7C26 /* XCTElementSetTransformer-Protocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "XCTElementSetTransformer-Protocol.h"; sourceTree = "<group>"; };
1FC3B2E12121EC8C00B61EE0 /* FBApplicationProcessProxyTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBApplicationProcessProxyTests.m; sourceTree = "<group>"; };
44757A831D42CE8300ECF35E /* XCUIDeviceRotationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = XCUIDeviceRotationTests.m; sourceTree = "<group>"; };
631B523421F6174300625362 /* FBImageIOScalerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBImageIOScalerTests.m; sourceTree = "<group>"; };
63CCF91021ECE4C700E94ABD /* FBImageIOScaler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBImageIOScaler.h; sourceTree = "<group>"; };
63CCF91121ECE4C700E94ABD /* FBImageIOScaler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBImageIOScaler.m; sourceTree = "<group>"; };
710181F7211DF584002FD3A8 /* CocoaAsyncSocket.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CocoaAsyncSocket.framework; path = Carthage/Build/iOS/CocoaAsyncSocket.framework; sourceTree = "<group>"; };
71018200211DF62C002FD3A8 /* libxml2.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libxml2.tbd; path = usr/lib/libxml2.tbd; sourceTree = SDKROOT; };
7101820C211E026B002FD3A8 /* libAccessibility.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libAccessibility.tbd; path = usr/lib/libAccessibility.tbd; sourceTree = SDKROOT; };
Expand Down Expand Up @@ -1154,6 +1162,8 @@
711084431DA3AA7500F913D6 /* FBXPath.m */,
EE6B64FB1D0F86EF00E85F5D /* XCTestPrivateSymbols.h */,
EE6B64FC1D0F86EF00E85F5D /* XCTestPrivateSymbols.m */,
63CCF91021ECE4C700E94ABD /* FBImageIOScaler.h */,
63CCF91121ECE4C700E94ABD /* FBImageIOScaler.m */,
);
name = Utilities;
path = WebDriverAgentLib/Utilities;
Expand Down Expand Up @@ -1213,6 +1223,7 @@
71E504941DF59BAD0020C32A /* XCUIElementAttributesTests.m */,
EEBBD48D1D4785FC00656A81 /* XCUIElementFBFindTests.m */,
EE1E06E11D181CC9007CF043 /* XCUIElementHelperIntegrationTests.m */,
631B523421F6174300625362 /* FBImageIOScalerTests.m */,
);
path = IntegrationTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -1560,6 +1571,7 @@
71BD20731F86116100B36EC2 /* XCUIApplication+FBTouchAction.h in Headers */,
EE158ACE1CBD456F00A3E3F0 /* FBCommandHandler.h in Headers */,
EE158AC81CBD456F00A3E3F0 /* FBSessionCommands.h in Headers */,
63CCF91221ECE4C700E94ABD /* FBImageIOScaler.h in Headers */,
EE158AE31CBD456F00A3E3F0 /* FBSession-Private.h in Headers */,
716E0BCE1E917E810087A825 /* NSString+FBXMLSafeString.h in Headers */,
EE158ACF1CBD456F00A3E3F0 /* FBCommandStatus.h in Headers */,
Expand Down Expand Up @@ -1921,6 +1933,7 @@
EEE3764A1D59FAE900ED88DD /* XCUIElement+FBWebDriverAttributes.m in Sources */,
EE8DDD7E20C5733C004D4925 /* XCUIElement+FBForceTouch.m in Sources */,
71241D7C1FAE3D2500B9559F /* FBTouchActionCommands.m in Sources */,
63CCF91321ECE4C700E94ABD /* FBImageIOScaler.m in Sources */,
EE158ACB1CBD456F00A3E3F0 /* FBTouchIDCommands.m in Sources */,
EE158ABD1CBD456F00A3E3F0 /* FBDebugCommands.m in Sources */,
716E0BCF1E917E810087A825 /* NSString+FBXMLSafeString.m in Sources */,
Expand Down Expand Up @@ -1967,6 +1980,7 @@
files = (
71241D801FAF087500B9559F /* FBW3CMultiTouchActionsIntegrationTests.m in Sources */,
71241D7E1FAF084E00B9559F /* FBW3CTouchActionsIntegrationTests.m in Sources */,
63FD950221F9D06100A3E356 /* FBImageIOScalerTests.m in Sources */,
719CD8FF2126C90200C7D0C2 /* FBAutoAlertsHandlerTests.m in Sources */,
EE2202131ECC612200A29571 /* FBIntegrationTestCase.m in Sources */,
71BD20781F869E0F00B36EC2 /* FBAppiumTouchActionsIntegrationTests.m in Sources */,
Expand All @@ -1983,6 +1997,7 @@
buildActionMask = 2147483647;
files = (
EE5095E51EBCC9090028E2FE /* FBTypingTest.m in Sources */,
63FD950321F9D06100A3E356 /* FBImageIOScalerTests.m in Sources */,
EE5095EB1EBCC9090028E2FE /* XCElementSnapshotHitPointTests.m in Sources */,
EE5095EC1EBCC9090028E2FE /* XCUIApplicationHelperTests.m in Sources */,
EE5095ED1EBCC9090028E2FE /* XCElementSnapshotHelperTests.m in Sources */,
Expand Down Expand Up @@ -2046,6 +2061,7 @@
EE26409D1D0EBA25009BE6B0 /* FBElementAttributeTests.m in Sources */,
7119E1EC1E891F8600D0B125 /* FBPickerWheelSelectTests.m in Sources */,
EE1E06DA1D1808C2007CF043 /* FBIntegrationTestCase.m in Sources */,
63FD950421F9D06200A3E356 /* FBImageIOScalerTests.m in Sources */,
EE05BAFA1D13003C00A3EB00 /* FBKeyboardTests.m in Sources */,
EE55B3271D1D54CF003AAAEC /* FBScrollingTests.m in Sources */,
EE6A89371D0B35920083E92B /* FBFailureProofTestCaseTests.m in Sources */,
Expand Down
6 changes: 6 additions & 0 deletions WebDriverAgentLib/Commands/FBSessionCommands.m
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
static NSString* const ELEMENT_RESPONSE_ATTRIBUTES = @"elementResponseAttributes";
static NSString* const MJPEG_SERVER_SCREENSHOT_QUALITY = @"mjpegServerScreenshotQuality";
static NSString* const MJPEG_SERVER_FRAMERATE = @"mjpegServerFramerate";
static NSString* const MJPEG_SCALING_FACTOR = @"mjpegScalingFactor";
static NSString* const MJPEG_COMPRESSION_FACTOR = @"mjpegCompressionFactor";
static NSString* const SCREENSHOT_QUALITY = @"screenshotQuality";

@implementation FBSessionCommands
Expand Down Expand Up @@ -204,6 +206,7 @@ + (NSArray *)routes
ELEMENT_RESPONSE_ATTRIBUTES: [FBConfiguration elementResponseAttributes],
MJPEG_SERVER_SCREENSHOT_QUALITY: @([FBConfiguration mjpegServerScreenshotQuality]),
MJPEG_SERVER_FRAMERATE: @([FBConfiguration mjpegServerFramerate]),
MJPEG_SCALING_FACTOR: @([FBConfiguration mjpegScalingFactor]),
SCREENSHOT_QUALITY: @([FBConfiguration screenshotQuality]),
}
);
Expand All @@ -230,6 +233,9 @@ + (NSArray *)routes
if ([settings objectForKey:SCREENSHOT_QUALITY]) {
[FBConfiguration setScreenshotQuality:[[settings objectForKey:SCREENSHOT_QUALITY] unsignedIntegerValue]];
}
if ([settings objectForKey:MJPEG_SCALING_FACTOR]) {
[FBConfiguration setMjpegScalingFactor:[[settings objectForKey:MJPEG_SCALING_FACTOR] unsignedIntegerValue]];
}

return [self handleGetSettings:request];
}
Expand Down
14 changes: 14 additions & 0 deletions WebDriverAgentLib/Routing/FBWebServer.m
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ - (void)startHTTPServer

- (void)initScreenshotsBroadcaster
{
[self readMjpegSettingsFromEnv];
self.screenshotsBroadcaster = [[FBTCPSocket alloc]
initWithPort:(uint16_t)FBConfiguration.mjpegServerPort];
self.screenshotsBroadcaster.delegate = [[FBMjpegServer alloc] init];
Expand All @@ -134,6 +135,19 @@ - (void)stopScreenshotsBroadcaster
[self.screenshotsBroadcaster stop];
}

- (void)readMjpegSettingsFromEnv
{
NSDictionary *env = NSProcessInfo.processInfo.environment;
NSString *scalingFactor = [env objectForKey:@"MJPEG_SCALING_FACTOR"];
if (scalingFactor != nil && [scalingFactor length] > 0) {
[FBConfiguration setMjpegScalingFactor:[scalingFactor integerValue]];
}
NSString *screenshotQuality = [env objectForKey:@"MJPEG_SERVER_SCREENSHOT_QUALITY"];
if (screenshotQuality != nil && [screenshotQuality length] > 0) {
[FBConfiguration setMjpegServerScreenshotQuality:[screenshotQuality integerValue]];
}
}

- (void)stopServing
{
[FBSession.activeSession kill];
Expand Down
6 changes: 6 additions & 0 deletions WebDriverAgentLib/Utilities/FBConfiguration.h
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@ NS_ASSUME_NONNULL_BEGIN
*/
+ (NSInteger)mjpegServerPort;

/**
The scaling factor for frames of the mjpeg stream (Default values is 100 and does not perform scaling).
*/
+ (NSUInteger)mjpegScalingFactor;
+ (void)setMjpegScalingFactor:(NSUInteger)scalingFactor;

/**
YES if verbose logging is enabled. NO otherwise.
*/
Expand Down
10 changes: 10 additions & 0 deletions WebDriverAgentLib/Utilities/FBConfiguration.m
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
static NSUInteger FBMjpegServerScreenshotQuality = 25;
static NSUInteger FBMjpegServerFramerate = 10;
static NSUInteger FBScreenshotQuality = 1;
static NSUInteger FBMjpegScalingFactor = 100;

@implementation FBConfiguration

Expand Down Expand Up @@ -74,6 +75,15 @@ + (NSInteger)mjpegServerPort
return DefaultMjpegServerPort;
}

+ (NSUInteger)mjpegScalingFactor
{
return FBMjpegScalingFactor;
}

+ (void)setMjpegScalingFactor:(NSUInteger)scalingFactor {
FBMjpegScalingFactor = scalingFactor;
}

+ (BOOL)verboseLoggingEnabled
{
return [NSProcessInfo.processInfo.environment[@"VERBOSE_LOGGING"] boolValue];
Expand Down
40 changes: 40 additions & 0 deletions WebDriverAgentLib/Utilities/FBImageIOScaler.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/

#import <Foundation/Foundation.h>
#import <CoreGraphics/CoreGraphics.h>

NS_ASSUME_NONNULL_BEGIN

// Those values define the allowed ranges for the scaling factor and compression quality settings
extern const CGFloat FBMinScalingFactor;
extern const CGFloat FBMaxScalingFactor;
extern const CGFloat FBMinCompressionQuality;
extern const CGFloat FBMaxCompressionQuality;


/**
Scales images and compresses it to JPEG using Image I/O
It allows to enqueue only a single screenshot. If a new one arrives before the currently queued gets discared
*/
@interface FBImageIOScaler : NSObject

/**
Puts the passed image on the queue and dispatches a scaling operation. If there is already a image on the
queue it will be replaced with the new one
@param image The image to scale down
@param completionHandler called after successfully scaling down an image
@param scalingFactor the scaling factor in range 0.01..1.0. A value of 1.0 won't perform scaling at all
@param compressionQuality the compression quality in range 0.0..1.0 (0.0 for max. compression and 1.0 for lossless compression)
*/
- (void)submitImage:(NSData *)image scalingFactor:(CGFloat)scalingFactor compressionQuality:(CGFloat)compressionQuality completionHandler:(void (^)(NSData *))completionHandler;

@end

NS_ASSUME_NONNULL_END
127 changes: 127 additions & 0 deletions WebDriverAgentLib/Utilities/FBImageIOScaler.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/

#import "FBImageIOScaler.h"
#import <ImageIO/ImageIO.h>
#import <MobileCoreServices/MobileCoreServices.h>
#import "FBLogger.h"

const CGFloat FBMinScalingFactor = 0.01f;
const CGFloat FBMaxScalingFactor = 1.0f;
const CGFloat FBMinCompressionQuality = 0.0f;
const CGFloat FBMaxCompressionQuality = 1.0f;

@interface FBImageIOScaler ()

@property (nonatomic) NSData *nextImage;
@property (nonatomic, readonly) NSLock *nextImageLock;
@property (nonatomic, readonly) dispatch_queue_t scalingQueue;

@end

@implementation FBImageIOScaler

- (id)init
{
self = [super init];
if (self) {
_nextImageLock = [[NSLock alloc] init];
_scalingQueue = dispatch_queue_create("image.scaling.queue", NULL);
}
return self;
}

- (void)submitImage:(NSData *)image scalingFactor:(CGFloat)scalingFactor compressionQuality:(CGFloat)compressionQuality completionHandler:(void (^)(NSData *))completionHandler {
[self.nextImageLock lock];
if (self.nextImage != nil) {
[FBLogger verboseLog:@"Discarding screenshot"];
}
scalingFactor = MAX(FBMinScalingFactor, MIN(FBMaxScalingFactor, scalingFactor));
compressionQuality = MAX(FBMinCompressionQuality, MIN(FBMaxCompressionQuality, compressionQuality));
self.nextImage = image;
[self.nextImageLock unlock];

dispatch_async(self.scalingQueue, ^{
[self.nextImageLock lock];
NSData *next = self.nextImage;
self.nextImage = nil;
[self.nextImageLock unlock];
if (next == nil) {
return;
}
NSData *scaled = [self scaledImageWithImage:next
scalingFactor:scalingFactor
compressionQuality:compressionQuality];
if (scaled == nil) {
[FBLogger log:@"Could not scale down the image"];
return;
}
completionHandler(scaled);
});
}

- (nullable NSData *)scaledImageWithImage:(NSData *)image scalingFactor:(CGFloat)scalingFactor compressionQuality:(CGFloat)compressionQuality {
CGImageSourceRef imageData = CGImageSourceCreateWithData((CFDataRef)image, nil);

CGSize size = [FBImageIOScaler imageSizeWithImage:imageData];
CGFloat scaledMaxPixelSize = MAX(size.width, size.height) * scalingFactor;

CFDictionaryRef params = (__bridge CFDictionaryRef)@{
(const NSString *)kCGImageSourceCreateThumbnailWithTransform: @(YES),
(const NSString *)kCGImageSourceCreateThumbnailFromImageIfAbsent: @(YES),
(const NSString *)kCGImageSourceThumbnailMaxPixelSize: @(scaledMaxPixelSize)
};

CGImageRef scaled = CGImageSourceCreateThumbnailAtIndex(imageData, 0, params);
if (scaled == nil) {
[FBLogger log:@"Failed to scale the image"];
CFRelease(imageData);
return nil;
}
NSData *jpegData = [self jpegDataWithImage:scaled
compressionQuality:compressionQuality];
CGImageRelease(scaled);
CFRelease(imageData);
return jpegData;
}

- (nullable NSData *)jpegDataWithImage:(CGImageRef)imageRef compressionQuality:(CGFloat)compressionQuality
{
NSMutableData *newImageData = [NSMutableData data];
CGImageDestinationRef imageDestination = CGImageDestinationCreateWithData((CFMutableDataRef)newImageData, kUTTypeJPEG, 1, NULL);

CFDictionaryRef compressionOptions = (__bridge CFDictionaryRef)@{
(const NSString *)kCGImageDestinationLossyCompressionQuality: @(compressionQuality)
};

CGImageDestinationAddImage(imageDestination, imageRef, compressionOptions);
if(!CGImageDestinationFinalize(imageDestination)) {
[FBLogger log:@"Failed to write the image"];
newImageData = nil;
}
CFRelease(imageDestination);
return newImageData;
}

+ (CGSize)imageSizeWithImage:(CGImageSourceRef)imageSource
{
NSDictionary *options = @{
(const NSString *)kCGImageSourceShouldCache: @(NO)
};
CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, (CFDictionaryRef)options);

NSNumber *width = [(__bridge NSDictionary *)properties objectForKey:(const NSString *)kCGImagePropertyPixelWidth];
NSNumber *height = [(__bridge NSDictionary *)properties objectForKey:(const NSString *)kCGImagePropertyPixelHeight];

CGSize size = CGSizeMake([width floatValue], [height floatValue]);
CFRelease(properties);
return size;
}

@end
Loading

0 comments on commit c83f796

Please sign in to comment.