appium-webdriveragent 16.5.0 → 16.6.0
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 +12 -0
- package/WebDriverAgent.xcodeproj/project.pbxproj +6 -0
- package/WebDriverAgentLib/Info.plist +2 -2
- package/WebDriverAgentLib/Routing/FBTCPSocket.h +7 -0
- package/WebDriverAgentLib/Routing/FBTCPSocket.m +5 -2
- package/WebDriverAgentLib/Routing/FBWebServer.m +5 -10
- package/WebDriverAgentLib/Utilities/FBMjpegServer.h +9 -0
- package/WebDriverAgentLib/Utilities/FBMjpegServer.m +64 -0
- package/WebDriverAgentTests/IntegrationTests_watchOS/IntegrationTests_watchOS-Bridging-Header.h +2 -0
- package/WebDriverAgentTests/IntegrationTests_watchOS/WDAMjpegStreamingIntegrationTests.swift +81 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## [16.6.0](https://github.com/appium/WebDriverAgent/compare/v16.5.1...v16.6.0) (2026-08-22)
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
* Add MJPEG screenshot streaming support to watchOS ([#1220](https://github.com/appium/WebDriverAgent/issues/1220)) ([c6dcf03](https://github.com/appium/WebDriverAgent/commit/c6dcf03096dd5faab83398f16fb772724fcc5220))
|
|
6
|
+
|
|
7
|
+
## [16.5.1](https://github.com/appium/WebDriverAgent/compare/v16.5.0...v16.5.1) (2026-08-20)
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* add watchOS assets to GitHub release artifacts ([#1219](https://github.com/appium/WebDriverAgent/issues/1219)) ([ce5a9e8](https://github.com/appium/WebDriverAgent/commit/ce5a9e8af36747a72669c34b38bd39e495b57f5b))
|
|
12
|
+
|
|
1
13
|
## [16.5.0](https://github.com/appium/WebDriverAgent/compare/v16.4.0...v16.5.0) (2026-08-20)
|
|
2
14
|
|
|
3
15
|
### Features
|
|
@@ -310,6 +310,7 @@
|
|
|
310
310
|
641EE5F62240C5CA00173FCB /* FBRouteRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7881CAEDF0C008C271F /* FBRouteRequest.m */; };
|
|
311
311
|
641EE5F72240C5CA00173FCB /* FBResponseJSONPayload.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7811CAEDF0C008C271F /* FBResponseJSONPayload.m */; };
|
|
312
312
|
641EE5F92240C5CA00173FCB /* FBMjpegServer.m in Sources */ = {isa = PBXBuildFile; fileRef = 7155D702211DCEF400166C20 /* FBMjpegServer.m */; };
|
|
313
|
+
AA11BB22CC33DD44EE55FF02 /* FBMjpegServer.m in Sources */ = {isa = PBXBuildFile; fileRef = 7155D702211DCEF400166C20 /* FBMjpegServer.m */; };
|
|
313
314
|
641EE5FA2240C5CA00173FCB /* XCUIDevice+FBHealthCheck.m in Sources */ = {isa = PBXBuildFile; fileRef = EEDFE1201D9C06F800E6FFE5 /* XCUIDevice+FBHealthCheck.m */; };
|
|
314
315
|
641EE5FD2240C5CA00173FCB /* FBBaseActionsSynthesizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 7140974D1FAE20EE008FB2C5 /* FBBaseActionsSynthesizer.m */; };
|
|
315
316
|
641EE5FE2240C5CA00173FCB /* XCUIElement+FBWebDriverAttributes.m in Sources */ = {isa = PBXBuildFile; fileRef = EEE376481D59FAE900ED88DD /* XCUIElement+FBWebDriverAttributes.m */; };
|
|
@@ -1320,6 +1321,7 @@
|
|
|
1320
1321
|
FBDF04E5916B91FCEC0190CB /* Route.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DCA324913C210060D7EB /* Route.h */; };
|
|
1321
1322
|
FBF064E4D96CFCD08E1F4EF7 /* XCUIDevice.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACFD1E3B77D600A02D78 /* XCUIDevice.h */; };
|
|
1322
1323
|
FBFEC05ED2C01D1EAE34BE9A /* WDAScreenshotAndSourceIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75F677B5B7737E7E1F321C20 /* WDAScreenshotAndSourceIntegrationTests.swift */; };
|
|
1324
|
+
AA11BB22CC33DD44EE55FF04 /* WDAMjpegStreamingIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA11BB22CC33DD44EE55FF03 /* WDAMjpegStreamingIntegrationTests.swift */; };
|
|
1323
1325
|
FC607DB5132FEF425237267B /* XCTMessagingRole_MemoryTesting-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 13D863EAE7F6F8B7E42D99B9 /* XCTMessagingRole_MemoryTesting-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
|
1324
1326
|
FC73EE781D24A598B44C3337 /* XCUIElement+FBScrolling.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB74A1CAEDF0C008C271F /* XCUIElement+FBScrolling.m */; };
|
|
1325
1327
|
FD89236D119E129E8CEBDBCD /* XCTMessagingRole_HIDEventRecording-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 94D7F0C1E30FBDB5D2583908 /* XCTMessagingRole_HIDEventRecording-Protocol.h */; };
|
|
@@ -1739,6 +1741,7 @@
|
|
|
1739
1741
|
75438FC693C39052C82C27DB /* XCUIApplicationRegistry.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = XCUIApplicationRegistry.h; sourceTree = "<group>"; };
|
|
1740
1742
|
758FC0D745C185A2C28BEDA1 /* IntegrationApp_watchOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = IntegrationApp_watchOS.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
|
1741
1743
|
75F677B5B7737E7E1F321C20 /* WDAScreenshotAndSourceIntegrationTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WDAScreenshotAndSourceIntegrationTests.swift; sourceTree = "<group>"; };
|
|
1744
|
+
AA11BB22CC33DD44EE55FF03 /* WDAMjpegStreamingIntegrationTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WDAMjpegStreamingIntegrationTests.swift; sourceTree = "<group>"; };
|
|
1742
1745
|
7AA21CEBA6E92AAC73FB6A48 /* XCTAggregateSuiteRunStatistics.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = XCTAggregateSuiteRunStatistics.h; sourceTree = "<group>"; };
|
|
1743
1746
|
7CBAA574F7786985E01D0B6F /* XCTHarnessEventReporting-Protocol.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "XCTHarnessEventReporting-Protocol.h"; sourceTree = "<group>"; };
|
|
1744
1747
|
7E079E00FE148476F94BC42F /* XCTTestIdentifierSet.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = XCTTestIdentifierSet.h; sourceTree = "<group>"; };
|
|
@@ -2503,6 +2506,7 @@
|
|
|
2503
2506
|
C878996F07A9B26E66FC4EAC /* WDAClickIntegrationTests.swift */,
|
|
2504
2507
|
912F32C353FE7C3E7C841405 /* WDATypingIntegrationTests.swift */,
|
|
2505
2508
|
75F677B5B7737E7E1F321C20 /* WDAScreenshotAndSourceIntegrationTests.swift */,
|
|
2509
|
+
AA11BB22CC33DD44EE55FF03 /* WDAMjpegStreamingIntegrationTests.swift */,
|
|
2506
2510
|
EB1B9A793C9EFB7853C1AA13 /* WDAAppLifecycleIntegrationTests.swift */,
|
|
2507
2511
|
646D714BC17FE9258CFBDF55 /* WDADeviceIntegrationTests.swift */,
|
|
2508
2512
|
28F75AEC6442F32A3C4394AD /* WDAAlertIntegrationTests.swift */,
|
|
@@ -4741,6 +4745,7 @@
|
|
|
4741
4745
|
6AF8892AEF5C6BF01896B6DA /* WDAAppLifecycleIntegrationTests.swift in Sources */,
|
|
4742
4746
|
6EDF83DA0A6C7A7E67C30AF6 /* WDADeviceIntegrationTests.swift in Sources */,
|
|
4743
4747
|
FBFEC05ED2C01D1EAE34BE9A /* WDAScreenshotAndSourceIntegrationTests.swift in Sources */,
|
|
4748
|
+
AA11BB22CC33DD44EE55FF04 /* WDAMjpegStreamingIntegrationTests.swift in Sources */,
|
|
4744
4749
|
D22FED0B3856919DA4BCFA67 /* WDATypingIntegrationTests.swift in Sources */,
|
|
4745
4750
|
68EB0B8D53270CB68C53D028 /* WDAAlertIntegrationTests.swift in Sources */,
|
|
4746
4751
|
);
|
|
@@ -4862,6 +4867,7 @@
|
|
|
4862
4867
|
C2D22426DB9FCE6AD84FA16A /* RouteRequest.m in Sources */,
|
|
4863
4868
|
3FEF512914A962C5E30579FC /* RouteResponse.m in Sources */,
|
|
4864
4869
|
91FF05D51401D1F1A88FB0B0 /* FBWatchHTTPServer.m in Sources */,
|
|
4870
|
+
AA11BB22CC33DD44EE55FF02 /* FBMjpegServer.m in Sources */,
|
|
4865
4871
|
);
|
|
4866
4872
|
runOnlyForDeploymentPostprocessing = 0;
|
|
4867
4873
|
};
|
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
<key>CFBundlePackageType</key>
|
|
16
16
|
<string>FMWK</string>
|
|
17
17
|
<key>CFBundleShortVersionString</key>
|
|
18
|
-
<string>16.
|
|
18
|
+
<string>16.6.0</string>
|
|
19
19
|
<key>CFBundleSignature</key>
|
|
20
20
|
<string>????</string>
|
|
21
21
|
<key>CFBundleVersion</key>
|
|
22
|
-
<string>16.
|
|
22
|
+
<string>16.6.0</string>
|
|
23
23
|
<key>NSPrincipalClass</key>
|
|
24
24
|
<string/>
|
|
25
25
|
</dict>
|
|
@@ -92,6 +92,13 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
92
92
|
@property (nullable, nonatomic, assign) id<FBTCPSocketDelegate> delegate;
|
|
93
93
|
#endif
|
|
94
94
|
|
|
95
|
+
/**
|
|
96
|
+
The port this socket is listening on. Equal to the port passed to -initWithPort: unless that was
|
|
97
|
+
0 ("let the system assign a port"), in which case this reflects the actually assigned port once
|
|
98
|
+
-startWithError: has returned successfully.
|
|
99
|
+
*/
|
|
100
|
+
@property (nonatomic, readonly) uint16_t port;
|
|
101
|
+
|
|
95
102
|
/**
|
|
96
103
|
Creates TCP socket isntance which is going to be started on the specified port
|
|
97
104
|
|
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
@property (readonly, nonatomic) dispatch_queue_t socketQueue;
|
|
15
15
|
@property (nullable, nonatomic) nw_listener_t listener;
|
|
16
16
|
@property (readonly, nonatomic) NSMutableArray<nw_connection_t> *connectedClients;
|
|
17
|
-
@property (readonly, nonatomic) uint16_t port;
|
|
18
17
|
@end
|
|
19
18
|
|
|
20
19
|
|
|
@@ -61,6 +60,10 @@
|
|
|
61
60
|
// if/else, not switch: -Wswitch-enum, -Wswitch-default, and -Wcovered-switch-default can't
|
|
62
61
|
// all be satisfied by one switch statement at once.
|
|
63
62
|
if (nw_listener_state_ready == state) {
|
|
63
|
+
__strong typeof(weakSelf) strongSelf = weakSelf;
|
|
64
|
+
if (strongSelf) {
|
|
65
|
+
strongSelf->_port = nw_listener_get_port(listener);
|
|
66
|
+
}
|
|
64
67
|
dispatch_semaphore_signal(startupSemaphore);
|
|
65
68
|
} else if (nw_listener_state_failed == state || nw_listener_state_cancelled == state) {
|
|
66
69
|
// NSLocalizedDescriptionKey must be a string, not the underlying NSError itself, or
|
|
@@ -210,7 +213,6 @@
|
|
|
210
213
|
@property (readonly, nonatomic) dispatch_queue_t socketQueue;
|
|
211
214
|
@property (readonly, nonatomic) GCDAsyncSocket *listeningSocket;
|
|
212
215
|
@property (readonly, nonatomic) NSMutableArray *connectedClients;
|
|
213
|
-
@property (readonly, nonatomic) uint16_t port;
|
|
214
216
|
@end
|
|
215
217
|
|
|
216
218
|
|
|
@@ -239,6 +241,7 @@
|
|
|
239
241
|
return NO;
|
|
240
242
|
}
|
|
241
243
|
|
|
244
|
+
_port = self.listeningSocket.localPort;
|
|
242
245
|
return YES;
|
|
243
246
|
}
|
|
244
247
|
|
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
#else
|
|
14
14
|
#import "RoutingConnection.h"
|
|
15
15
|
#import "RoutingHTTPServer.h"
|
|
16
|
+
#endif
|
|
16
17
|
#import "FBMjpegServer.h"
|
|
17
18
|
#import "FBTCPSocket.h"
|
|
18
|
-
#endif
|
|
19
19
|
|
|
20
20
|
#import "FBCommandHandler.h"
|
|
21
21
|
#import "FBErrorBuilder.h"
|
|
@@ -59,9 +59,9 @@ static NSString *const FBServerURLEndMarker = @"<-ServerURLHere";
|
|
|
59
59
|
@property (nonatomic, strong) FBWatchHTTPServer *server;
|
|
60
60
|
#else
|
|
61
61
|
@property (nonatomic, strong) RoutingHTTPServer *server;
|
|
62
|
+
#endif
|
|
62
63
|
@property (nonatomic, nullable) FBTCPSocket *screenshotsBroadcaster;
|
|
63
64
|
@property (nonatomic, nullable, strong) FBMjpegServer *mjpegServer;
|
|
64
|
-
#endif
|
|
65
65
|
@property (atomic, assign) BOOL keepAlive;
|
|
66
66
|
@end
|
|
67
67
|
|
|
@@ -69,9 +69,7 @@ static NSString *const FBServerURLEndMarker = @"<-ServerURLHere";
|
|
|
69
69
|
|
|
70
70
|
- (void)dealloc
|
|
71
71
|
{
|
|
72
|
-
#if !TARGET_OS_WATCH
|
|
73
72
|
[self stopScreenshotsBroadcaster];
|
|
74
|
-
#endif
|
|
75
73
|
}
|
|
76
74
|
|
|
77
75
|
+ (NSArray<Class<FBCommandHandler>> *)collectCommandHandlerClasses
|
|
@@ -96,9 +94,7 @@ static NSString *const FBServerURLEndMarker = @"<-ServerURLHere";
|
|
|
96
94
|
if (![self startHTTPServer]) {
|
|
97
95
|
return;
|
|
98
96
|
}
|
|
99
|
-
#if !TARGET_OS_WATCH
|
|
100
97
|
[self initScreenshotsBroadcaster];
|
|
101
|
-
#endif
|
|
102
98
|
|
|
103
99
|
self.keepAlive = YES;
|
|
104
100
|
NSRunLoop *runLoop = [NSRunLoop mainRunLoop];
|
|
@@ -163,13 +159,15 @@ static NSString *const FBServerURLEndMarker = @"<-ServerURLHere";
|
|
|
163
159
|
return YES;
|
|
164
160
|
}
|
|
165
161
|
|
|
166
|
-
#if !TARGET_OS_WATCH
|
|
167
162
|
- (void)initScreenshotsBroadcaster
|
|
168
163
|
{
|
|
169
164
|
[self readMjpegSettingsFromEnv];
|
|
170
165
|
self.mjpegServer = [[FBMjpegServer alloc] init];
|
|
171
166
|
self.screenshotsBroadcaster = [[FBTCPSocket alloc]
|
|
172
167
|
initWithPort:(uint16_t)FBConfiguration.sharedInstance.mjpegServerPort];
|
|
168
|
+
#if TARGET_OS_WATCH
|
|
169
|
+
self.mjpegServer.socket = self.screenshotsBroadcaster;
|
|
170
|
+
#endif
|
|
173
171
|
self.screenshotsBroadcaster.delegate = self.mjpegServer;
|
|
174
172
|
NSError *error;
|
|
175
173
|
if (![self.screenshotsBroadcaster startWithError:&error]) {
|
|
@@ -209,14 +207,11 @@ static NSString *const FBServerURLEndMarker = @"<-ServerURLHere";
|
|
|
209
207
|
FBConfiguration.sharedInstance.mjpegServerScreenshotQuality = [screenshotQuality integerValue];
|
|
210
208
|
}
|
|
211
209
|
}
|
|
212
|
-
#endif
|
|
213
210
|
|
|
214
211
|
- (void)stopServing
|
|
215
212
|
{
|
|
216
213
|
[FBSession.activeSession kill];
|
|
217
|
-
#if !TARGET_OS_WATCH
|
|
218
214
|
[self stopScreenshotsBroadcaster];
|
|
219
|
-
#endif
|
|
220
215
|
if (self.server.isRunning) {
|
|
221
216
|
[self.server stop:NO];
|
|
222
217
|
}
|
|
@@ -19,6 +19,15 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
19
19
|
*/
|
|
20
20
|
- (instancetype)init;
|
|
21
21
|
|
|
22
|
+
#if TARGET_OS_WATCH
|
|
23
|
+
/**
|
|
24
|
+
The socket that owns this instance as its delegate. watchOS clients are bare nw_connection_t
|
|
25
|
+
values with no write method of their own (unlike GCDAsyncSocket on other platforms), so frame
|
|
26
|
+
writes are routed through -[FBTCPSocket writeData:toClient:]. Must be set before streaming starts.
|
|
27
|
+
*/
|
|
28
|
+
@property (nonatomic, weak, nullable) FBTCPSocket *socket;
|
|
29
|
+
#endif
|
|
30
|
+
|
|
22
31
|
/**
|
|
23
32
|
Stops screenshot broadcasting and prevents future scheduling.
|
|
24
33
|
*/
|
|
@@ -11,7 +11,12 @@
|
|
|
11
11
|
#import <mach/mach_time.h>
|
|
12
12
|
@import UniformTypeIdentifiers;
|
|
13
13
|
|
|
14
|
+
#if TARGET_OS_WATCH
|
|
15
|
+
// Textual import, not `@import Network;` - see the comment in FBTCPSocket.h.
|
|
16
|
+
#import <Network/Network.h>
|
|
17
|
+
#else
|
|
14
18
|
#import "GCDAsyncSocket.h"
|
|
19
|
+
#endif
|
|
15
20
|
#import "FBConfiguration.h"
|
|
16
21
|
#import "FBLogger.h"
|
|
17
22
|
#import "FBScreenshot.h"
|
|
@@ -36,7 +41,11 @@ static NSUInteger FBNormalizedMjpegFramerate(NSUInteger framerate)
|
|
|
36
41
|
@interface FBMjpegServer()
|
|
37
42
|
|
|
38
43
|
@property (nonatomic, readonly) dispatch_queue_t backgroundQueue;
|
|
44
|
+
#if TARGET_OS_WATCH
|
|
45
|
+
@property (nonatomic, readonly) NSMutableArray<nw_connection_t> *listeningClients;
|
|
46
|
+
#else
|
|
39
47
|
@property (nonatomic, readonly) NSMutableArray<GCDAsyncSocket *> *listeningClients;
|
|
48
|
+
#endif
|
|
40
49
|
@property (nonatomic, readonly) FBImageProcessor *imageProcessor;
|
|
41
50
|
@property (nonatomic, readonly) long long mainScreenID;
|
|
42
51
|
@property (nonatomic, assign) NSUInteger consecutiveScreenshotFailures;
|
|
@@ -148,10 +157,16 @@ static NSUInteger FBNormalizedMjpegFramerate(NSUInteger framerate)
|
|
|
148
157
|
return;
|
|
149
158
|
}
|
|
150
159
|
NSUInteger clientCount = self.listeningClients.count;
|
|
160
|
+
#if TARGET_OS_WATCH
|
|
161
|
+
for (nw_connection_t client in self.listeningClients) {
|
|
162
|
+
[self.socket writeData:chunk toClient:client];
|
|
163
|
+
}
|
|
164
|
+
#else
|
|
151
165
|
for (GCDAsyncSocket *client in self.listeningClients) {
|
|
152
166
|
// Slow clients should fail/close instead of buffering indefinitely.
|
|
153
167
|
[client writeData:chunk withTimeout:FRAME_TIMEOUT tag:0];
|
|
154
168
|
}
|
|
169
|
+
#endif
|
|
155
170
|
self.sentFramesCount++;
|
|
156
171
|
self.sentBytesCount += chunk.length * clientCount;
|
|
157
172
|
NSUInteger framerate = FBNormalizedMjpegFramerate(FBConfiguration.sharedInstance.mjpegServerFramerate);
|
|
@@ -164,6 +179,53 @@ static NSUInteger FBNormalizedMjpegFramerate(NSUInteger framerate)
|
|
|
164
179
|
}
|
|
165
180
|
}
|
|
166
181
|
|
|
182
|
+
#if TARGET_OS_WATCH
|
|
183
|
+
|
|
184
|
+
- (void)didClientConnect:(nw_connection_t)newClient
|
|
185
|
+
{
|
|
186
|
+
[FBLogger log:@"Got screenshots broadcast client connection"];
|
|
187
|
+
// FBTCPSocket already schedules the receive that -client:didReceiveData: relies on below;
|
|
188
|
+
// unlike GCDAsyncSocket, there is nothing to arm here.
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
- (void)client:(nw_connection_t)client didReceiveData:(NSData *)data
|
|
192
|
+
{
|
|
193
|
+
@synchronized (self.listeningClients) {
|
|
194
|
+
if ([self.listeningClients containsObject:client]) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
[FBLogger log:@"Starting screenshots broadcast for the client"];
|
|
200
|
+
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];
|
|
201
|
+
[self.socket writeData:(id)[streamHeader dataUsingEncoding:NSUTF8StringEncoding] toClient:client];
|
|
202
|
+
@synchronized (self.listeningClients) {
|
|
203
|
+
[self.listeningClients addObject:client];
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
- (void)didClientDisconnect:(nw_connection_t)client
|
|
208
|
+
{
|
|
209
|
+
@synchronized (self.listeningClients) {
|
|
210
|
+
[self.listeningClients removeObject:client];
|
|
211
|
+
}
|
|
212
|
+
[FBLogger log:@"Disconnected a client from screenshots broadcast"];
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
- (void)stopStreaming
|
|
216
|
+
{
|
|
217
|
+
self.isStreaming = NO;
|
|
218
|
+
@synchronized (self.listeningClients) {
|
|
219
|
+
NSArray<nw_connection_t> *clients = self.listeningClients.copy;
|
|
220
|
+
[self.listeningClients removeAllObjects];
|
|
221
|
+
for (nw_connection_t client in clients) {
|
|
222
|
+
nw_connection_cancel(client);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
#else
|
|
228
|
+
|
|
167
229
|
- (void)didClientConnect:(GCDAsyncSocket *)newClient
|
|
168
230
|
{
|
|
169
231
|
[FBLogger logFmt:@"Got screenshots broadcast client connection at %@:%d", newClient.connectedHost, newClient.connectedPort];
|
|
@@ -207,6 +269,8 @@ static NSUInteger FBNormalizedMjpegFramerate(NSUInteger framerate)
|
|
|
207
269
|
}
|
|
208
270
|
}
|
|
209
271
|
|
|
272
|
+
#endif
|
|
273
|
+
|
|
210
274
|
- (void)dealloc
|
|
211
275
|
{
|
|
212
276
|
[self stopStreaming];
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2015-present, Facebook, Inc.
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the BSD-style license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import Network
|
|
10
|
+
import XCTest
|
|
11
|
+
|
|
12
|
+
/// Exercises FBMjpegServer+FBTCPSocket the same way FBWebServer wires them together in
|
|
13
|
+
/// -initScreenshotsBroadcaster, but standalone (no full FBWebServer/HTTP command server needed).
|
|
14
|
+
/// A client NWConnection reads the raw wire bytes back to confirm the multipart/x-mixed-replace
|
|
15
|
+
/// stream and its JPEG frames actually arrive, since watchOS has no BSD sockets/GCDAsyncSocket -
|
|
16
|
+
/// the production implementation is Network.framework-backed and worth covering for real.
|
|
17
|
+
final class WDAMjpegStreamingIntegrationTests: WDAWatchInProcessTestCase {
|
|
18
|
+
override class var relaunchForEachTest: Bool { false }
|
|
19
|
+
|
|
20
|
+
private static let jpegMagicBytes: [UInt8] = [0xFF, 0xD8, 0xFF]
|
|
21
|
+
|
|
22
|
+
func testMjpegStreamingProducesJpegFrames() throws {
|
|
23
|
+
// Port 0 asks the system to assign an ephemeral port; -[FBTCPSocket port] then reflects the
|
|
24
|
+
// actual bound port once -start returns, per nw_listener_get_port's documented behavior.
|
|
25
|
+
let socket = FBTCPSocket(port: 0)
|
|
26
|
+
let server = FBMjpegServer()
|
|
27
|
+
server.socket = socket
|
|
28
|
+
socket.delegate = server
|
|
29
|
+
defer {
|
|
30
|
+
server.stopStreaming()
|
|
31
|
+
socket.stop()
|
|
32
|
+
}
|
|
33
|
+
try socket.start()
|
|
34
|
+
let boundPort = socket.port
|
|
35
|
+
XCTAssertNotEqual(boundPort, 0, "FBTCPSocket did not report its bound ephemeral port")
|
|
36
|
+
|
|
37
|
+
let connection = NWConnection(host: "127.0.0.1", port: NWEndpoint.Port(rawValue: boundPort)!, using: .tcp)
|
|
38
|
+
defer { connection.cancel() }
|
|
39
|
+
|
|
40
|
+
let connected = expectation(description: "client connected to the mjpeg broadcaster")
|
|
41
|
+
connection.stateUpdateHandler = { state in
|
|
42
|
+
if case .ready = state {
|
|
43
|
+
connected.fulfill()
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
connection.start(queue: .main)
|
|
47
|
+
wait(for: [connected], timeout: 15)
|
|
48
|
+
|
|
49
|
+
// FBMjpegServer only starts broadcasting once it has seen any data from the client - mimic the
|
|
50
|
+
// request line a real HTTP client would send.
|
|
51
|
+
let sent = expectation(description: "sent the request line")
|
|
52
|
+
connection.send(content: Data("GET / HTTP/1.0\r\n\r\n".utf8), completion: .contentProcessed { _ in
|
|
53
|
+
sent.fulfill()
|
|
54
|
+
})
|
|
55
|
+
wait(for: [sent], timeout: 15)
|
|
56
|
+
|
|
57
|
+
var received = Data()
|
|
58
|
+
let jpegMagicData = Data(Self.jpegMagicBytes)
|
|
59
|
+
let sawJpegFrame = expectation(description: "received at least one jpeg frame")
|
|
60
|
+
func receiveMore() {
|
|
61
|
+
connection.receive(minimumIncompleteLength: 1, maximumLength: 65536) { data, _, _, error in
|
|
62
|
+
if let data = data {
|
|
63
|
+
received.append(data)
|
|
64
|
+
if received.range(of: jpegMagicData) != nil {
|
|
65
|
+
sawJpegFrame.fulfill()
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if error == nil {
|
|
70
|
+
receiveMore()
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
receiveMore()
|
|
75
|
+
wait(for: [sawJpegFrame], timeout: 15)
|
|
76
|
+
|
|
77
|
+
XCTAssertNotNil(received.range(of: Data("multipart/x-mixed-replace".utf8)), "Response is not a multipart stream")
|
|
78
|
+
XCTAssertNotNil(received.range(of: Data("Content-type: image/jpeg".utf8)), "No JPEG part header found")
|
|
79
|
+
XCTAssertNotNil(received.range(of: jpegMagicData), "No JPEG frame data found")
|
|
80
|
+
}
|
|
81
|
+
}
|