appium-webdriveragent 16.9.3 → 16.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## [16.9.4](https://github.com/appium/WebDriverAgent/compare/v16.9.3...v16.9.4) (2026-08-29)
2
+
3
+ ### Bug Fixes
4
+
5
+ * drop MJPEG frames for clients that stop draining their socket ([#1227](https://github.com/appium/WebDriverAgent/issues/1227)) ([00cb6b6](https://github.com/appium/WebDriverAgent/commit/00cb6b612a5a1a1d00a386e23de01c4eca811df8))
6
+
1
7
  ## [16.9.3](https://github.com/appium/WebDriverAgent/compare/v16.9.2...v16.9.3) (2026-08-29)
2
8
 
3
9
  ### Bug Fixes
@@ -15,11 +15,11 @@
15
15
  <key>CFBundlePackageType</key>
16
16
  <string>FMWK</string>
17
17
  <key>CFBundleShortVersionString</key>
18
- <string>16.9.3</string>
18
+ <string>16.9.4</string>
19
19
  <key>CFBundleSignature</key>
20
20
  <string>????</string>
21
21
  <key>CFBundleVersion</key>
22
- <string>16.9.3</string>
22
+ <string>16.9.4</string>
23
23
  <key>NSPrincipalClass</key>
24
24
  <string/>
25
25
  </dict>
@@ -22,6 +22,9 @@
22
22
 
23
23
  static const NSUInteger MAX_FPS = 60;
24
24
  static const NSTimeInterval FRAME_TIMEOUT = 1.;
25
+ // nw_connection_send buffers without backpressure, so a client that stops reading would retain
26
+ // every generated frame. Frames past this cap are dropped instead of queued.
27
+ static const NSUInteger MAX_PENDING_FRAMES_PER_CLIENT = 4;
25
28
  static const NSTimeInterval FAILURE_BACKOFF_MIN = 1.0;
26
29
  static const NSTimeInterval FAILURE_BACKOFF_MAX = 10.0;
27
30
 
@@ -44,6 +47,9 @@ static NSUInteger FBNormalizedMjpegFramerate(NSUInteger framerate)
44
47
  @property (atomic, assign) BOOL isStreaming;
45
48
  @property (nonatomic, assign) NSUInteger sentFramesCount;
46
49
  @property (nonatomic, assign) NSUInteger sentBytesCount;
50
+ @property (nonatomic, assign) NSUInteger droppedFramesCount;
51
+ // Frames submitted but not sent yet, per client. Guarded by @synchronized (self.listeningClients).
52
+ @property (nonatomic, readonly) NSMapTable<id, NSNumber *> *pendingFrameCounts;
47
53
 
48
54
  @end
49
55
 
@@ -58,6 +64,8 @@ static NSUInteger FBNormalizedMjpegFramerate(NSUInteger framerate)
58
64
  _sentFramesCount = 0;
59
65
  _sentBytesCount = 0;
60
66
  _listeningClients = [NSMutableArray array];
67
+ _pendingFrameCounts = [NSMapTable mapTableWithKeyOptions:(NSPointerFunctionsOptions)(NSMapTableObjectPointerPersonality | NSMapTableStrongMemory)
68
+ valueOptions:(NSPointerFunctionsOptions)NSMapTableStrongMemory];
61
69
  _imageProcessor = [[FBImageProcessor alloc] init];
62
70
  _mainScreenID = [XCUIScreen.mainScreen displayID];
63
71
  dispatch_queue_attr_t queueAttributes = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_UTILITY, 0);
@@ -149,17 +157,38 @@ static NSUInteger FBNormalizedMjpegFramerate(NSUInteger framerate)
149
157
  return;
150
158
  }
151
159
  NSUInteger clientCount = self.listeningClients.count;
160
+ __weak typeof(self) weakSelf = self;
152
161
  for (nw_connection_t client in self.listeningClients) {
153
- [self.socket writeData:chunk toClient:client];
162
+ NSUInteger pendingFrames = [self.pendingFrameCounts objectForKey:client].unsignedIntegerValue;
163
+ if (pendingFrames >= MAX_PENDING_FRAMES_PER_CLIENT) {
164
+ self.droppedFramesCount++;
165
+ continue;
166
+ }
167
+ [self.pendingFrameCounts setObject:@(pendingFrames + 1) forKey:client];
168
+ [self.socket writeData:chunk toClient:client completion:^{
169
+ __strong typeof(weakSelf) strongSelf = weakSelf;
170
+ if (nil == strongSelf) {
171
+ return;
172
+ }
173
+ @synchronized (strongSelf.listeningClients) {
174
+ NSUInteger stillPending = [strongSelf.pendingFrameCounts objectForKey:client].unsignedIntegerValue;
175
+ if (stillPending > 1) {
176
+ [strongSelf.pendingFrameCounts setObject:@(stillPending - 1) forKey:client];
177
+ } else {
178
+ [strongSelf.pendingFrameCounts removeObjectForKey:client];
179
+ }
180
+ }
181
+ }];
154
182
  }
155
183
  self.sentFramesCount++;
156
184
  self.sentBytesCount += chunk.length * clientCount;
157
185
  NSUInteger framerate = FBNormalizedMjpegFramerate(FBConfiguration.sharedInstance.mjpegServerFramerate);
158
186
  if (0 == self.sentFramesCount % framerate) {
159
- [FBLogger verboseLog:[NSString stringWithFormat:@"MJPEG stats: clients=%@ sentFrames=%@ sentBytes=%@",
187
+ [FBLogger verboseLog:[NSString stringWithFormat:@"MJPEG stats: clients=%@ sentFrames=%@ sentBytes=%@ droppedFrames=%@",
160
188
  @(clientCount),
161
189
  @(self.sentFramesCount),
162
- @(self.sentBytesCount)]];
190
+ @(self.sentBytesCount),
191
+ @(self.droppedFramesCount)]];
163
192
  }
164
193
  }
165
194
  }
@@ -190,6 +219,7 @@ static NSUInteger FBNormalizedMjpegFramerate(NSUInteger framerate)
190
219
  {
191
220
  @synchronized (self.listeningClients) {
192
221
  [self.listeningClients removeObject:client];
222
+ [self.pendingFrameCounts removeObjectForKey:client];
193
223
  }
194
224
  [FBLogger log:@"Disconnected a client from screenshots broadcast"];
195
225
  }
@@ -200,6 +230,7 @@ static NSUInteger FBNormalizedMjpegFramerate(NSUInteger framerate)
200
230
  @synchronized (self.listeningClients) {
201
231
  NSArray<nw_connection_t> *clients = self.listeningClients.copy;
202
232
  [self.listeningClients removeAllObjects];
233
+ [self.pendingFrameCounts removeAllObjects];
203
234
  for (nw_connection_t client in clients) {
204
235
  nw_connection_cancel(client);
205
236
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appium-webdriveragent",
3
- "version": "16.9.3",
3
+ "version": "16.9.4",
4
4
  "description": "Package bundling WebDriverAgent",
5
5
  "keywords": [
6
6
  "Appium",