Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow image scaling on the mjepg stream #138

Merged
merged 24 commits into from
Feb 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
b7e7f69
Scale image on the mjpeg-stream with Image I/O
dmissmann Jan 7, 2019
1680edb
use NSUInteger
dmissmann Jan 15, 2019
037cf0a
renamed methods
dmissmann Jan 15, 2019
7d2c654
made imageSizeWithImage: a class method
dmissmann Jan 15, 2019
460b398
moved curly braces
dmissmann Jan 15, 2019
dc4e8fe
added docs and check for valid values
dmissmann Jan 15, 2019
05b6fb0
check if scaling is enabled
dmissmann Jan 15, 2019
77dca7b
typo
dmissmann Jan 21, 2019
8bfc49a
added test for the scaler
dmissmann Jan 21, 2019
47b2884
read env variables and call setters
dmissmann Jan 24, 2019
fdae9b4
allow dynamic changes of mjpeg scaling and compression
dmissmann Jan 24, 2019
22bac0b
typo
dmissmann Jan 24, 2019
78a4299
check env variables length
dmissmann Jan 26, 2019
eec117c
use constants for min-max values
dmissmann Jan 26, 2019
c4b9a8d
calc scalingfactor only once
dmissmann Jan 26, 2019
6b65907
inherit from FBIntegrationTestCase
dmissmann Jan 26, 2019
2c36d35
added nullable annotations
dmissmann Jan 26, 2019
c2dcdf4
fixed dependencies and xcode 9.2 build errors
dmissmann Jan 27, 2019
d9043ed
renamed test
dmissmann Jan 27, 2019
97ab4d2
removed the 'with' from the method name
dmissmann Jan 28, 2019
e30ff85
use the same compression quality for scaling as in the mjpeg server
dmissmann Jan 28, 2019
6308603
do a lossless compression before scaling
dmissmann Feb 4, 2019
e94b474
use CGImageRelease
dmissmann Feb 4, 2019
cb47679
use constants for checking if scaling should be applied or not
dmissmann Feb 5, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"];
mykola-mokhnach marked this conversation as resolved.
Show resolved Hide resolved
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];

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need to recompress the original image after it has been already compressed by XCTest? Wouldn't this be a waste of resources?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, otherwise the size keeps roughly the same.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the additional setting for compression quality and now we use the same value for screenshot taking and when we create the jpeg after scaling

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