From 2f2086434f9d681d0f48797e95d9ae61234f36a0 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Thu, 9 Apr 2026 17:37:06 +0200 Subject: [PATCH 1/9] fix: Avoid keeping strong reference to self instance in delegates --- WebDriverAgentLib/Routing/FBTCPSocket.h | 6 ++- WebDriverAgentLib/Routing/FBTCPSocket.m | 21 ++++++-- WebDriverAgentLib/Routing/FBWebServer.m | 31 +++++++++-- .../Utilities/FBImageProcessor.m | 53 ++++++++++++------- WebDriverAgentLib/Utilities/FBMjpegServer.h | 5 ++ WebDriverAgentLib/Utilities/FBMjpegServer.m | 53 +++++++++++++++++-- 6 files changed, 135 insertions(+), 34 deletions(-) diff --git a/WebDriverAgentLib/Routing/FBTCPSocket.h b/WebDriverAgentLib/Routing/FBTCPSocket.h index 31adc7e22..72947e2a0 100644 --- a/WebDriverAgentLib/Routing/FBTCPSocket.h +++ b/WebDriverAgentLib/Routing/FBTCPSocket.h @@ -38,7 +38,11 @@ NS_ASSUME_NONNULL_BEGIN @interface FBTCPSocket : NSObject -@property (nullable, nonatomic) id delegate; +#if __has_feature(objc_arc_weak) +@property (nullable, nonatomic, weak) id delegate; +#else +@property (nullable, nonatomic, assign) id delegate; +#endif /** Creates TCP socket isntance which is going to be started on the specified port diff --git a/WebDriverAgentLib/Routing/FBTCPSocket.m b/WebDriverAgentLib/Routing/FBTCPSocket.m index fabd22074..c6c6b6805 100644 --- a/WebDriverAgentLib/Routing/FBTCPSocket.m +++ b/WebDriverAgentLib/Routing/FBTCPSocket.m @@ -48,11 +48,13 @@ - (BOOL)startWithError:(NSError **)error - (void)stop { @synchronized(self.connectedClients) { - for (NSUInteger i = 0; i < [self.connectedClients count]; i++) { - [[self.connectedClients objectAtIndex:i] disconnect]; + NSArray *clients = self.connectedClients.copy; + for (GCDAsyncSocket *client in clients) { + [client disconnect]; } } + self.delegate = nil; [self.listeningSocket disconnect]; } @@ -66,12 +68,18 @@ - (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSo @synchronized(self.connectedClients) { [self.connectedClients addObject:newSocket]; } - [self.delegate didClientConnect:newSocket]; + id delegate = self.delegate; + if (nil != delegate) { + [delegate didClientConnect:newSocket]; + } } - (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag { - [self.delegate didClientSendData:sock]; + id delegate = self.delegate; + if (nil != delegate) { + [delegate didClientSendData:sock]; + } } - (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err @@ -79,7 +87,10 @@ - (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err @synchronized(self.connectedClients) { [self.connectedClients removeObject:sock]; } - [self.delegate didClientDisconnect:sock]; + id delegate = self.delegate; + if (nil != delegate) { + [delegate didClientDisconnect:sock]; + } } @end diff --git a/WebDriverAgentLib/Routing/FBWebServer.m b/WebDriverAgentLib/Routing/FBWebServer.m index 0b82eaff5..ea39147f2 100644 --- a/WebDriverAgentLib/Routing/FBWebServer.m +++ b/WebDriverAgentLib/Routing/FBWebServer.m @@ -51,6 +51,11 @@ @interface FBWebServer () @implementation FBWebServer +- (void)dealloc +{ + [self stopScreenshotsBroadcaster]; +} + + (NSArray> *)collectCommandHandlerClasses { NSArray *handlersClasses = FBClassesThatConformsToProtocol(@protocol(FBCommandHandler)); @@ -125,12 +130,14 @@ - (void)startHTTPServer - (void)initScreenshotsBroadcaster { [self readMjpegSettingsFromEnv]; + FBMjpegServer *mjpegServer = [[FBMjpegServer alloc] init]; self.screenshotsBroadcaster = [[FBTCPSocket alloc] initWithPort:(uint16_t)FBConfiguration.mjpegServerPort]; - self.screenshotsBroadcaster.delegate = [[FBMjpegServer alloc] init]; + self.screenshotsBroadcaster.delegate = mjpegServer; NSError *error; if (![self.screenshotsBroadcaster startWithError:&error]) { [FBLogger logFmt:@"Cannot init screenshots broadcaster service on port %@. Original error: %@", @(FBConfiguration.mjpegServerPort), error.description]; + [mjpegServer stopStreaming]; self.screenshotsBroadcaster = nil; } } @@ -141,7 +148,13 @@ - (void)stopScreenshotsBroadcaster return; } + id delegate = self.screenshotsBroadcaster.delegate; + if ([delegate respondsToSelector:@selector(stopStreaming)]) { + [(FBMjpegServer *)delegate stopStreaming]; + } + self.screenshotsBroadcaster.delegate = nil; [self.screenshotsBroadcaster stop]; + self.screenshotsBroadcaster = nil; } - (void)readMjpegSettingsFromEnv @@ -164,6 +177,8 @@ - (void)stopServing if (self.server.isRunning) { [self.server stop:NO]; } + self.server = nil; + self.exceptionHandler = nil; self.keepAlive = NO; } @@ -192,10 +207,15 @@ - (BOOL)attemptToStartServer:(RoutingHTTPServer *)server onPort:(NSInteger)port - (void)registerRouteHandlers:(NSArray *)commandHandlerClasses { + __weak typeof(self) weakSelf = self; for (Class commandHandler in commandHandlerClasses) { NSArray *routes = [commandHandler routes]; for (FBRoute *route in routes) { [self.server handleMethod:route.verb withPath:route.path block:^(RouteRequest *request, RouteResponse *response) { + __strong typeof(weakSelf) strongSelf = weakSelf; + if (nil == strongSelf) { + return; + } NSDictionary *arguments = [NSJSONSerialization JSONObjectWithData:request.body options:NSJSONReadingMutableContainers error:NULL]; FBRouteRequest *routeParams = [FBRouteRequest routeRequestWithURL:request.url @@ -209,7 +229,7 @@ - (void)registerRouteHandlers:(NSArray *)commandHandlerClasses [route mountRequest:routeParams intoResponse:response]; } @catch (NSException *exception) { - [self handleException:exception forResponse:response]; + [strongSelf handleException:exception forResponse:response]; } }]; } @@ -237,9 +257,14 @@ - (void)registerServerKeyRouteHandlers [response respondWithString:calibrationPage]; }]; + __weak typeof(self) weakSelf = self; [self.server get:@"/wda/shutdown" withBlock:^(RouteRequest *request, RouteResponse *response) { + __strong typeof(weakSelf) strongSelf = weakSelf; + if (nil == strongSelf) { + return; + } [response respondWithString:@"Shutting down"]; - [self.delegate webServerDidRequestShutdown:self]; + [strongSelf.delegate webServerDidRequestShutdown:strongSelf]; }]; [self registerRouteHandlers:@[FBUnknownCommands.class]]; diff --git a/WebDriverAgentLib/Utilities/FBImageProcessor.m b/WebDriverAgentLib/Utilities/FBImageProcessor.m index 7d2ada759..6a9836532 100644 --- a/WebDriverAgentLib/Utilities/FBImageProcessor.m +++ b/WebDriverAgentLib/Utilities/FBImageProcessor.m @@ -27,6 +27,7 @@ @interface FBImageProcessor () @property (nonatomic) NSData *nextImage; @property (nonatomic, readonly) NSLock *nextImageLock; @property (nonatomic, readonly) dispatch_queue_t scalingQueue; +@property (atomic, assign) BOOL isScalingScheduled; @end @@ -38,6 +39,7 @@ - (id)init if (self) { _nextImageLock = [[NSLock alloc] init]; _scalingQueue = dispatch_queue_create("image.scaling.queue", NULL); + _isScalingScheduled = NO; } return self; } @@ -51,32 +53,43 @@ - (void)submitImageData:(NSData *)image [FBLogger verboseLog:@"Discarding screenshot"]; } self.nextImage = image; + BOOL shouldSchedule = !self.isScalingScheduled; + if (shouldSchedule) { + self.isScalingScheduled = YES; + } [self.nextImageLock unlock]; + if (!shouldSchedule) { + return; + } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wcompletion-handler" dispatch_async(self.scalingQueue, ^{ - [self.nextImageLock lock]; - NSData *nextImageData = self.nextImage; - self.nextImage = nil; - [self.nextImageLock unlock]; - if (nextImageData == nil) { - return; - } + while (YES) { + [self.nextImageLock lock]; + NSData *nextImageData = self.nextImage; + self.nextImage = nil; + if (nextImageData == nil) { + self.isScalingScheduled = NO; + [self.nextImageLock unlock]; + return; + } + [self.nextImageLock unlock]; - // We do not want this value to be too high because then we get images larger in size than original ones - // Although, we also don't want to lose too much of the quality on recompression - CGFloat recompressionQuality = MAX(0.9, - MIN(FBMaxCompressionQuality, FBConfiguration.mjpegServerScreenshotQuality / 100.0)); - NSData *thumbnailData = [self.class fixedImageDataWithImageData:nextImageData - scalingFactor:scalingFactor - uti:UTTypeJPEG - compressionQuality:recompressionQuality - // iOS always returns screnshots in portrait orientation, but puts the real value into the metadata - // Use it with care. See https://github.com/appium/WebDriverAgent/pull/812 - fixOrientation:FBConfiguration.mjpegShouldFixOrientation - desiredOrientation:nil]; - completionHandler(thumbnailData ?: nextImageData); + // We do not want this value to be too high because then we get images larger in size than original ones + // Although, we also don't want to lose too much of the quality on recompression + CGFloat recompressionQuality = MAX(0.9, + MIN(FBMaxCompressionQuality, FBConfiguration.mjpegServerScreenshotQuality / 100.0)); + NSData *thumbnailData = [self.class fixedImageDataWithImageData:nextImageData + scalingFactor:scalingFactor + uti:UTTypeJPEG + compressionQuality:recompressionQuality + // iOS always returns screnshots in portrait orientation, but puts the real value into the metadata + // Use it with care. See https://github.com/appium/WebDriverAgent/pull/812 + fixOrientation:FBConfiguration.mjpegShouldFixOrientation + desiredOrientation:nil]; + completionHandler(thumbnailData ?: nextImageData); + } }); #pragma clang diagnostic pop } diff --git a/WebDriverAgentLib/Utilities/FBMjpegServer.h b/WebDriverAgentLib/Utilities/FBMjpegServer.h index 294c399f8..a9b47cada 100644 --- a/WebDriverAgentLib/Utilities/FBMjpegServer.h +++ b/WebDriverAgentLib/Utilities/FBMjpegServer.h @@ -19,6 +19,11 @@ NS_ASSUME_NONNULL_BEGIN */ - (instancetype)init; +/** + Stops screenshot broadcasting and prevents future scheduling. + */ +- (void)stopStreaming; + @end NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBMjpegServer.m b/WebDriverAgentLib/Utilities/FBMjpegServer.m index 4f061d2e3..5053d1b8b 100644 --- a/WebDriverAgentLib/Utilities/FBMjpegServer.m +++ b/WebDriverAgentLib/Utilities/FBMjpegServer.m @@ -35,6 +35,9 @@ @interface FBMjpegServer() @property (nonatomic, readonly) FBImageProcessor *imageProcessor; @property (nonatomic, readonly) long long mainScreenID; @property (nonatomic, assign) NSUInteger consecutiveScreenshotFailures; +@property (atomic, assign) BOOL isStreaming; +@property (nonatomic, assign) NSUInteger sentFramesCount; +@property (nonatomic, assign) NSUInteger sentBytesCount; @end @@ -45,11 +48,15 @@ - (instancetype)init { if ((self = [super init])) { _consecutiveScreenshotFailures = 0; + _isStreaming = YES; + _sentFramesCount = 0; + _sentBytesCount = 0; _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); + __weak typeof(self) weakSelf = self; dispatch_async(_backgroundQueue, ^{ - [self streamScreenshot]; + [weakSelf streamScreenshot]; }); _imageProcessor = [[FBImageProcessor alloc] init]; _mainScreenID = [XCUIScreen.mainScreen displayID]; @@ -59,22 +66,29 @@ - (instancetype)init - (void)scheduleNextScreenshotWithInterval:(uint64_t)timerInterval timeStarted:(uint64_t)timeStarted { + if (!self.isStreaming) { + return; + } uint64_t timeElapsed = clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW) - timeStarted; int64_t nextTickDelta = timerInterval - timeElapsed; + __weak typeof(self) weakSelf = self; if (nextTickDelta > 0) { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, nextTickDelta), self.backgroundQueue, ^{ - [self streamScreenshot]; + [weakSelf streamScreenshot]; }); } else { // Try to do our best to keep the FPS at a decent level dispatch_async(self.backgroundQueue, ^{ - [self streamScreenshot]; + [weakSelf streamScreenshot]; }); } } - (void)streamScreenshot { + if (!self.isStreaming) { + return; + } 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); @@ -106,10 +120,11 @@ - (void)streamScreenshot self.consecutiveScreenshotFailures = 0; CGFloat scalingFactor = FBConfiguration.mjpegScalingFactor / 100.0; + __weak typeof(self) weakSelf = self; [self.imageProcessor submitImageData:screenshotData scalingFactor:scalingFactor completionHandler:^(NSData * _Nonnull scaled) { - [self sendScreenshot:scaled]; + [weakSelf sendScreenshot:scaled]; }]; [self scheduleNextScreenshotWithInterval:timerInterval timeStarted:timeStarted]; @@ -122,7 +137,17 @@ - (void)sendScreenshot:(NSData *)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]; + // Slow clients should fail/close instead of buffering indefinitely. + [client writeData:chunk withTimeout:FRAME_TIMEOUT tag:0]; + } + self.sentFramesCount++; + self.sentBytesCount += chunk.length * self.listeningClients.count; + NSUInteger framerate = MAX(1, MIN(MAX_FPS, FBConfiguration.mjpegServerFramerate)); + if (0 == self.sentFramesCount % framerate) { + [FBLogger verboseLog:[NSString stringWithFormat:@"MJPEG stats: clients=%@ sentFrames=%@ sentBytes=%@", + @(self.listeningClients.count), + @(self.sentFramesCount), + @(self.sentBytesCount)]]; } } } @@ -158,4 +183,22 @@ - (void)didClientDisconnect:(GCDAsyncSocket *)client [FBLogger log:@"Disconnected a client from screenshots broadcast"]; } +- (void)stopStreaming +{ + self.isStreaming = NO; + @synchronized (self.listeningClients) { + NSArray *clients = self.listeningClients.copy; + [self.listeningClients removeAllObjects]; + for (GCDAsyncSocket *client in clients) { + [client disconnect]; + } + } +} + +- (void)dealloc +{ + [self stopStreaming]; + [FBLogger verboseLog:@"FBMjpegServer deallocated"]; +} + @end From 720bfe25536d9184a58a4c8c911785976ea1169f Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Thu, 9 Apr 2026 17:46:37 +0200 Subject: [PATCH 2/9] Fix compilation --- WebDriverAgentLib/Routing/FBWebServer.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebDriverAgentLib/Routing/FBWebServer.m b/WebDriverAgentLib/Routing/FBWebServer.m index ea39147f2..b021c43b6 100644 --- a/WebDriverAgentLib/Routing/FBWebServer.m +++ b/WebDriverAgentLib/Routing/FBWebServer.m @@ -149,7 +149,7 @@ - (void)stopScreenshotsBroadcaster } id delegate = self.screenshotsBroadcaster.delegate; - if ([delegate respondsToSelector:@selector(stopStreaming)]) { + if ([(NSObject *)delegate respondsToSelector:@selector(stopStreaming)]) { [(FBMjpegServer *)delegate stopStreaming]; } self.screenshotsBroadcaster.delegate = nil; From 6bf09b84a7036744c36b6b0f1f612047f1a76737 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Fri, 10 Apr 2026 10:30:43 +0200 Subject: [PATCH 3/9] address comments --- WebDriverAgentLib/Routing/FBWebServer.m | 10 +++-- .../Utilities/FBImageProcessor.m | 44 ++++++++++--------- WebDriverAgentLib/Utilities/FBMjpegServer.m | 11 +++-- 3 files changed, 38 insertions(+), 27 deletions(-) diff --git a/WebDriverAgentLib/Routing/FBWebServer.m b/WebDriverAgentLib/Routing/FBWebServer.m index b021c43b6..1ca5aedba 100644 --- a/WebDriverAgentLib/Routing/FBWebServer.m +++ b/WebDriverAgentLib/Routing/FBWebServer.m @@ -47,6 +47,7 @@ @interface FBWebServer () @property (nonatomic, strong) RoutingHTTPServer *server; @property (atomic, assign) BOOL keepAlive; @property (nonatomic, nullable) FBTCPSocket *screenshotsBroadcaster; +@property (nonatomic, nullable, strong) FBMjpegServer *mjpegServer; @end @implementation FBWebServer @@ -130,14 +131,15 @@ - (void)startHTTPServer - (void)initScreenshotsBroadcaster { [self readMjpegSettingsFromEnv]; - FBMjpegServer *mjpegServer = [[FBMjpegServer alloc] init]; + self.mjpegServer = [[FBMjpegServer alloc] init]; self.screenshotsBroadcaster = [[FBTCPSocket alloc] initWithPort:(uint16_t)FBConfiguration.mjpegServerPort]; - self.screenshotsBroadcaster.delegate = mjpegServer; + self.screenshotsBroadcaster.delegate = self.mjpegServer; NSError *error; if (![self.screenshotsBroadcaster startWithError:&error]) { [FBLogger logFmt:@"Cannot init screenshots broadcaster service on port %@. Original error: %@", @(FBConfiguration.mjpegServerPort), error.description]; - [mjpegServer stopStreaming]; + [self.mjpegServer stopStreaming]; + self.mjpegServer = nil; self.screenshotsBroadcaster = nil; } } @@ -145,6 +147,7 @@ - (void)initScreenshotsBroadcaster - (void)stopScreenshotsBroadcaster { if (nil == self.screenshotsBroadcaster) { + self.mjpegServer = nil; return; } @@ -155,6 +158,7 @@ - (void)stopScreenshotsBroadcaster self.screenshotsBroadcaster.delegate = nil; [self.screenshotsBroadcaster stop]; self.screenshotsBroadcaster = nil; + self.mjpegServer = nil; } - (void)readMjpegSettingsFromEnv diff --git a/WebDriverAgentLib/Utilities/FBImageProcessor.m b/WebDriverAgentLib/Utilities/FBImageProcessor.m index 6a9836532..0e70fcc74 100644 --- a/WebDriverAgentLib/Utilities/FBImageProcessor.m +++ b/WebDriverAgentLib/Utilities/FBImageProcessor.m @@ -66,29 +66,31 @@ - (void)submitImageData:(NSData *)image #pragma clang diagnostic ignored "-Wcompletion-handler" dispatch_async(self.scalingQueue, ^{ while (YES) { - [self.nextImageLock lock]; - NSData *nextImageData = self.nextImage; - self.nextImage = nil; - if (nextImageData == nil) { - self.isScalingScheduled = NO; + @autoreleasepool { + [self.nextImageLock lock]; + NSData *nextImageData = self.nextImage; + self.nextImage = nil; + if (nextImageData == nil) { + self.isScalingScheduled = NO; + [self.nextImageLock unlock]; + return; + } [self.nextImageLock unlock]; - return; - } - [self.nextImageLock unlock]; - // We do not want this value to be too high because then we get images larger in size than original ones - // Although, we also don't want to lose too much of the quality on recompression - CGFloat recompressionQuality = MAX(0.9, - MIN(FBMaxCompressionQuality, FBConfiguration.mjpegServerScreenshotQuality / 100.0)); - NSData *thumbnailData = [self.class fixedImageDataWithImageData:nextImageData - scalingFactor:scalingFactor - uti:UTTypeJPEG - compressionQuality:recompressionQuality - // iOS always returns screnshots in portrait orientation, but puts the real value into the metadata - // Use it with care. See https://github.com/appium/WebDriverAgent/pull/812 - fixOrientation:FBConfiguration.mjpegShouldFixOrientation - desiredOrientation:nil]; - completionHandler(thumbnailData ?: nextImageData); + // We do not want this value to be too high because then we get images larger in size than original ones + // Although, we also don't want to lose too much of the quality on recompression + CGFloat recompressionQuality = MAX(0.9, + MIN(FBMaxCompressionQuality, FBConfiguration.mjpegServerScreenshotQuality / 100.0)); + NSData *thumbnailData = [self.class fixedImageDataWithImageData:nextImageData + scalingFactor:scalingFactor + uti:UTTypeJPEG + compressionQuality:recompressionQuality + // iOS always returns screnshots in portrait orientation, but puts the real value into the metadata + // Use it with care. See https://github.com/appium/WebDriverAgent/pull/812 + fixOrientation:FBConfiguration.mjpegShouldFixOrientation + desiredOrientation:nil]; + completionHandler(thumbnailData ?: nextImageData); + } } }); #pragma clang diagnostic pop diff --git a/WebDriverAgentLib/Utilities/FBMjpegServer.m b/WebDriverAgentLib/Utilities/FBMjpegServer.m index 5053d1b8b..ed9f341d8 100644 --- a/WebDriverAgentLib/Utilities/FBMjpegServer.m +++ b/WebDriverAgentLib/Utilities/FBMjpegServer.m @@ -27,6 +27,11 @@ static NSString *const SERVER_NAME = @"WDA MJPEG Server"; static const char *QUEUE_NAME = "JPEG Screenshots Provider Queue"; +static NSUInteger FBNormalizedMjpegFramerate(NSUInteger framerate) +{ + return (0 == framerate || framerate > MAX_FPS) ? MAX_FPS : framerate; +} + @interface FBMjpegServer() @@ -89,8 +94,8 @@ - (void)streamScreenshot if (!self.isStreaming) { return; } - NSUInteger framerate = FBConfiguration.mjpegServerFramerate; - uint64_t timerInterval = (uint64_t)(1.0 / ((0 == framerate || framerate > MAX_FPS) ? MAX_FPS : framerate) * NSEC_PER_SEC); + NSUInteger framerate = FBNormalizedMjpegFramerate(FBConfiguration.mjpegServerFramerate); + uint64_t timerInterval = (uint64_t)(1.0 / framerate * NSEC_PER_SEC); uint64_t timeStarted = clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW); @synchronized (self.listeningClients) { if (0 == self.listeningClients.count) { @@ -142,7 +147,7 @@ - (void)sendScreenshot:(NSData *)screenshotData { } self.sentFramesCount++; self.sentBytesCount += chunk.length * self.listeningClients.count; - NSUInteger framerate = MAX(1, MIN(MAX_FPS, FBConfiguration.mjpegServerFramerate)); + NSUInteger framerate = FBNormalizedMjpegFramerate(FBConfiguration.mjpegServerFramerate); if (0 == self.sentFramesCount % framerate) { [FBLogger verboseLog:[NSString stringWithFormat:@"MJPEG stats: clients=%@ sentFrames=%@ sentBytes=%@", @(self.listeningClients.count), From 830d55106e8a327735b04a12bd465f3fb052cb97 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Fri, 10 Apr 2026 10:31:41 +0200 Subject: [PATCH 4/9] moar --- WebDriverAgentLib/Utilities/FBMjpegServer.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WebDriverAgentLib/Utilities/FBMjpegServer.m b/WebDriverAgentLib/Utilities/FBMjpegServer.m index ed9f341d8..e230b1f7f 100644 --- a/WebDriverAgentLib/Utilities/FBMjpegServer.m +++ b/WebDriverAgentLib/Utilities/FBMjpegServer.m @@ -57,14 +57,14 @@ - (instancetype)init _sentFramesCount = 0; _sentBytesCount = 0; _listeningClients = [NSMutableArray array]; + _imageProcessor = [[FBImageProcessor alloc] init]; + _mainScreenID = [XCUIScreen.mainScreen displayID]; 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); __weak typeof(self) weakSelf = self; dispatch_async(_backgroundQueue, ^{ [weakSelf streamScreenshot]; }); - _imageProcessor = [[FBImageProcessor alloc] init]; - _mainScreenID = [XCUIScreen.mainScreen displayID]; } return self; } From b6cfe5cc682f54464f1b9f71af11a4d0d450ec54 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Fri, 10 Apr 2026 10:33:01 +0200 Subject: [PATCH 5/9] typo --- WebDriverAgentLib/Utilities/FBImageProcessor.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebDriverAgentLib/Utilities/FBImageProcessor.m b/WebDriverAgentLib/Utilities/FBImageProcessor.m index 0e70fcc74..951568a8e 100644 --- a/WebDriverAgentLib/Utilities/FBImageProcessor.m +++ b/WebDriverAgentLib/Utilities/FBImageProcessor.m @@ -85,7 +85,7 @@ - (void)submitImageData:(NSData *)image scalingFactor:scalingFactor uti:UTTypeJPEG compressionQuality:recompressionQuality - // iOS always returns screnshots in portrait orientation, but puts the real value into the metadata + // iOS always returns screenshots in portrait orientation, but puts the real value into the metadata // Use it with care. See https://github.com/appium/WebDriverAgent/pull/812 fixOrientation:FBConfiguration.mjpegShouldFixOrientation desiredOrientation:nil]; From e485a8bc3810c3912d73f4958dcc0d4defe243d3 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Fri, 10 Apr 2026 10:38:26 +0200 Subject: [PATCH 6/9] fix build --- lib/xcodebuild.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/xcodebuild.ts b/lib/xcodebuild.ts index f21289906..4ed4a8b91 100644 --- a/lib/xcodebuild.ts +++ b/lib/xcodebuild.ts @@ -461,7 +461,7 @@ export class XcodeBuild { } const proxyTimeout = noSessionProxy.timeout; - noSessionProxy.timeout = 1000; + (noSessionProxy as any).timeout = 1000; try { currentStatus = (await noSessionProxy.command('/status', 'GET')) as StringRecord; if (currentStatus && currentStatus.ios && (currentStatus.ios as any).ip) { @@ -472,7 +472,7 @@ export class XcodeBuild { } catch (err: any) { throw new Error(`Unable to connect to running WebDriverAgent: ${err.message}`); } finally { - noSessionProxy.timeout = proxyTimeout; + (noSessionProxy as any).timeout = proxyTimeout; } }); From e93099bd640cc9961764ea93ec551c822022c803 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Fri, 10 Apr 2026 10:40:48 +0200 Subject: [PATCH 7/9] fix build --- lib/xcodebuild.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/xcodebuild.ts b/lib/xcodebuild.ts index 4ed4a8b91..238d262c9 100644 --- a/lib/xcodebuild.ts +++ b/lib/xcodebuild.ts @@ -464,8 +464,8 @@ export class XcodeBuild { (noSessionProxy as any).timeout = 1000; try { currentStatus = (await noSessionProxy.command('/status', 'GET')) as StringRecord; - if (currentStatus && currentStatus.ios && (currentStatus.ios as any).ip) { - this.agentUrl = (currentStatus.ios as any).ip; + if (currentStatus?.ios?.ip) { + this.agentUrl = currentStatus.ios.ip as string | undefined; } this.log.debug(`WebDriverAgent information:`); this.log.debug(JSON.stringify(currentStatus, null, 2)); From 7ab55db2b0040bdc303cbee17199d60035b3ebea Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Fri, 10 Apr 2026 21:21:32 +0200 Subject: [PATCH 8/9] Address comments --- WebDriverAgentLib/Routing/FBWebServer.m | 4 ++-- WebDriverAgentLib/Utilities/FBMjpegServer.m | 13 ++++++++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/WebDriverAgentLib/Routing/FBWebServer.m b/WebDriverAgentLib/Routing/FBWebServer.m index 1ca5aedba..29a2e16e6 100644 --- a/WebDriverAgentLib/Routing/FBWebServer.m +++ b/WebDriverAgentLib/Routing/FBWebServer.m @@ -103,7 +103,7 @@ - (void)startHTTPServer [self.server setInterface:bindingIP]; [FBLogger logFmt:@"Using custom binding IP address: %@", bindingIP]; } - + NSError *error; BOOL serverStarted = NO; @@ -123,7 +123,7 @@ - (void)startHTTPServer [FBLogger logFmt:@"Last attempt to start web server failed with error %@", [error description]]; abort(); } - + NSString *serverHost = bindingIP ?: ([XCUIDevice sharedDevice].fb_wifiIPAddress ?: @"127.0.0.1"); [FBLogger logFmt:@"%@http://%@:%d%@", FBServerURLBeginMarker, serverHost, [self.server port], FBServerURLEndMarker]; } diff --git a/WebDriverAgentLib/Utilities/FBMjpegServer.m b/WebDriverAgentLib/Utilities/FBMjpegServer.m index e230b1f7f..75389df50 100644 --- a/WebDriverAgentLib/Utilities/FBMjpegServer.m +++ b/WebDriverAgentLib/Utilities/FBMjpegServer.m @@ -75,7 +75,7 @@ - (void)scheduleNextScreenshotWithInterval:(uint64_t)timerInterval timeStarted:( return; } uint64_t timeElapsed = clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW) - timeStarted; - int64_t nextTickDelta = timerInterval - timeElapsed; + int64_t nextTickDelta = (int64_t)timerInterval - (int64_t)timeElapsed; __weak typeof(self) weakSelf = self; if (nextTickDelta > 0) { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, nextTickDelta), self.backgroundQueue, ^{ @@ -136,21 +136,28 @@ - (void)streamScreenshot } - (void)sendScreenshot:(NSData *)screenshotData { + if (!self.isStreaming) { + return; + } 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) { + if (!self.isStreaming || 0 == self.listeningClients.count) { + return; + } + NSUInteger clientCount = self.listeningClients.count; for (GCDAsyncSocket *client in self.listeningClients) { // Slow clients should fail/close instead of buffering indefinitely. [client writeData:chunk withTimeout:FRAME_TIMEOUT tag:0]; } self.sentFramesCount++; - self.sentBytesCount += chunk.length * self.listeningClients.count; + self.sentBytesCount += chunk.length * clientCount; NSUInteger framerate = FBNormalizedMjpegFramerate(FBConfiguration.mjpegServerFramerate); if (0 == self.sentFramesCount % framerate) { [FBLogger verboseLog:[NSString stringWithFormat:@"MJPEG stats: clients=%@ sentFrames=%@ sentBytes=%@", - @(self.listeningClients.count), + @(clientCount), @(self.sentFramesCount), @(self.sentBytesCount)]]; } From 349878183750f1f07834a3f11ed59356a7f68c9d Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Sat, 11 Apr 2026 09:05:46 +0200 Subject: [PATCH 9/9] Address comments --- WebDriverAgentLib/Routing/FBTCPSocket.m | 1 + lib/xcodebuild.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/WebDriverAgentLib/Routing/FBTCPSocket.m b/WebDriverAgentLib/Routing/FBTCPSocket.m index c6c6b6805..25a286ddd 100644 --- a/WebDriverAgentLib/Routing/FBTCPSocket.m +++ b/WebDriverAgentLib/Routing/FBTCPSocket.m @@ -49,6 +49,7 @@ - (void)stop { @synchronized(self.connectedClients) { NSArray *clients = self.connectedClients.copy; + [self.connectedClients removeAllObjects]; for (GCDAsyncSocket *client in clients) { [client disconnect]; } diff --git a/lib/xcodebuild.ts b/lib/xcodebuild.ts index 238d262c9..754c87943 100644 --- a/lib/xcodebuild.ts +++ b/lib/xcodebuild.ts @@ -465,7 +465,7 @@ export class XcodeBuild { try { currentStatus = (await noSessionProxy.command('/status', 'GET')) as StringRecord; if (currentStatus?.ios?.ip) { - this.agentUrl = currentStatus.ios.ip as string | undefined; + this.agentUrl = currentStatus.ios.ip as string; } this.log.debug(`WebDriverAgent information:`); this.log.debug(JSON.stringify(currentStatus, null, 2));