| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152 |
- /**
- * 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 "FBMjpegServer.h"
- #import <mach/mach_time.h>
- @import UniformTypeIdentifiers;
- #import "GCDAsyncSocket.h"
- #import "FBConfiguration.h"
- #import "FBLogger.h"
- #import "FBScreenshot.h"
- #import "FBImageProcessor.h"
- #import "FBImageUtils.h"
- #import "XCUIScreen.h"
- static const NSUInteger MAX_FPS = 60;
- static const NSTimeInterval FRAME_TIMEOUT = 1.;
- static NSString *const SERVER_NAME = @"WDA MJPEG Server";
- static const char *QUEUE_NAME = "JPEG Screenshots Provider Queue";
- @interface FBMjpegServer()
- @property (nonatomic, readonly) dispatch_queue_t backgroundQueue;
- @property (nonatomic, readonly) NSMutableArray<GCDAsyncSocket *> *listeningClients;
- @property (nonatomic, readonly) FBImageProcessor *imageProcessor;
- @property (nonatomic, readonly) long long mainScreenID;
- @end
- @implementation FBMjpegServer
- - (instancetype)init
- {
- if ((self = [super init])) {
- _listeningClients = [NSMutableArray array];
- dispatch_queue_attr_t queueAttributes = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_UTILITY, 0);
- _backgroundQueue = dispatch_queue_create(QUEUE_NAME, queueAttributes);
- dispatch_async(_backgroundQueue, ^{
- [self streamScreenshot];
- });
- _imageProcessor = [[FBImageProcessor alloc] init];
- _mainScreenID = [XCUIScreen.mainScreen displayID];
- }
- return self;
- }
- - (void)scheduleNextScreenshotWithInterval:(uint64_t)timerInterval timeStarted:(uint64_t)timeStarted
- {
- uint64_t timeElapsed = clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW) - timeStarted;
- int64_t nextTickDelta = timerInterval - timeElapsed;
- if (nextTickDelta > 0) {
- dispatch_after(dispatch_time(DISPATCH_TIME_NOW, nextTickDelta), self.backgroundQueue, ^{
- [self streamScreenshot];
- });
- } else {
- // Try to do our best to keep the FPS at a decent level
- dispatch_async(self.backgroundQueue, ^{
- [self streamScreenshot];
- });
- }
- }
- - (void)streamScreenshot
- {
- NSUInteger framerate = FBConfiguration.mjpegServerFramerate;
- uint64_t timerInterval = (uint64_t)(1.0 / ((0 == framerate || framerate > MAX_FPS) ? MAX_FPS : framerate) * NSEC_PER_SEC);
- uint64_t timeStarted = clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW);
- @synchronized (self.listeningClients) {
- if (0 == self.listeningClients.count) {
- [self scheduleNextScreenshotWithInterval:timerInterval timeStarted:timeStarted];
- return;
- }
- }
- NSError *error;
- CGFloat compressionQuality = MAX(FBMinCompressionQuality,
- MIN(FBMaxCompressionQuality, FBConfiguration.mjpegServerScreenshotQuality / 100.0));
- NSData *screenshotData = [FBScreenshot takeInOriginalResolutionWithScreenID:self.mainScreenID
- compressionQuality:compressionQuality
- uti:UTTypeJPEG
- timeout:FRAME_TIMEOUT
- error:&error];
- if (nil == screenshotData) {
- [FBLogger logFmt:@"%@", error.description];
- [self scheduleNextScreenshotWithInterval:timerInterval timeStarted:timeStarted];
- return;
- }
- CGFloat scalingFactor = FBConfiguration.mjpegScalingFactor / 100.0;
- [self.imageProcessor submitImageData:screenshotData
- scalingFactor:scalingFactor
- completionHandler:^(NSData * _Nonnull scaled) {
- [self sendScreenshot:scaled];
- }];
- [self scheduleNextScreenshotWithInterval:timerInterval timeStarted:timeStarted];
- }
- - (void)sendScreenshot:(NSData *)screenshotData {
- NSString *chunkHeader = [NSString stringWithFormat:@"--BoundaryString\r\nContent-type: image/jpeg\r\nContent-Length: %@\r\n\r\n", @(screenshotData.length)];
- NSMutableData *chunk = [[chunkHeader dataUsingEncoding:NSUTF8StringEncoding] mutableCopy];
- [chunk appendData:screenshotData];
- [chunk appendData:(id)[@"\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
- @synchronized (self.listeningClients) {
- for (GCDAsyncSocket *client in self.listeningClients) {
- [client writeData:chunk withTimeout:-1 tag:0];
- }
- }
- }
- - (void)didClientConnect:(GCDAsyncSocket *)newClient
- {
- [FBLogger logFmt:@"Got screenshots broadcast client connection at %@:%d", newClient.connectedHost, newClient.connectedPort];
- // Start broadcast only after there is any data from the client
- [newClient readDataWithTimeout:-1 tag:0];
- }
- - (void)didClientSendData:(GCDAsyncSocket *)client
- {
- @synchronized (self.listeningClients) {
- if ([self.listeningClients containsObject:client]) {
- return;
- }
- }
- [FBLogger logFmt:@"Starting screenshots broadcast for the client at %@:%d", client.connectedHost, client.connectedPort];
- NSString *streamHeader = [NSString stringWithFormat:@"HTTP/1.0 200 OK\r\nServer: %@\r\nConnection: close\r\nMax-Age: 0\r\nExpires: 0\r\nCache-Control: no-cache, private\r\nPragma: no-cache\r\nContent-Type: multipart/x-mixed-replace; boundary=--BoundaryString\r\n\r\n", SERVER_NAME];
- [client writeData:(id)[streamHeader dataUsingEncoding:NSUTF8StringEncoding] withTimeout:-1 tag:0];
- @synchronized (self.listeningClients) {
- [self.listeningClients addObject:client];
- }
- }
- - (void)didClientDisconnect:(GCDAsyncSocket *)client
- {
- @synchronized (self.listeningClients) {
- [self.listeningClients removeObject:client];
- }
- [FBLogger log:@"Disconnected a client from screenshots broadcast"];
- }
- @end
|