@cloudcare/cocos-session-replay 0.1.0-alpha.6

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.
Files changed (169) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +53 -0
  3. package/THIRD_PARTY_NOTICES +5 -0
  4. package/components/creator2/ReplayPrivacy.ts +23 -0
  5. package/components/creator3/ReplayPrivacy.ts +25 -0
  6. package/creator2/package.json +4 -0
  7. package/creator3/package.json +5 -0
  8. package/dist/creator2/core/client.d.ts +30 -0
  9. package/dist/creator2/core/client.d.ts.map +1 -0
  10. package/dist/creator2/core/client.js +115 -0
  11. package/dist/creator2/core/client.js.map +1 -0
  12. package/dist/creator2/core/replay-async-transport.d.ts +4 -0
  13. package/dist/creator2/core/replay-async-transport.d.ts.map +1 -0
  14. package/dist/creator2/core/replay-async-transport.js +67 -0
  15. package/dist/creator2/core/replay-async-transport.js.map +1 -0
  16. package/dist/creator2/core/replay-budget.d.ts +28 -0
  17. package/dist/creator2/core/replay-budget.d.ts.map +1 -0
  18. package/dist/creator2/core/replay-budget.js +126 -0
  19. package/dist/creator2/core/replay-budget.js.map +1 -0
  20. package/dist/creator2/core/replay-diagnostics.d.ts +36 -0
  21. package/dist/creator2/core/replay-diagnostics.d.ts.map +1 -0
  22. package/dist/creator2/core/replay-diagnostics.js +40 -0
  23. package/dist/creator2/core/replay-diagnostics.js.map +1 -0
  24. package/dist/creator2/core/replay-encoding.d.ts +31 -0
  25. package/dist/creator2/core/replay-encoding.d.ts.map +1 -0
  26. package/dist/creator2/core/replay-encoding.js +63 -0
  27. package/dist/creator2/core/replay-encoding.js.map +1 -0
  28. package/dist/creator2/core/replay-file.d.ts +5 -0
  29. package/dist/creator2/core/replay-file.d.ts.map +1 -0
  30. package/dist/creator2/core/replay-file.js +101 -0
  31. package/dist/creator2/core/replay-file.js.map +1 -0
  32. package/dist/creator2/core/replay-pixels.d.ts +3 -0
  33. package/dist/creator2/core/replay-pixels.d.ts.map +1 -0
  34. package/dist/creator2/core/replay-pixels.js +16 -0
  35. package/dist/creator2/core/replay-pixels.js.map +1 -0
  36. package/dist/creator2/core/replay-privacy-nodes.d.ts +14 -0
  37. package/dist/creator2/core/replay-privacy-nodes.d.ts.map +1 -0
  38. package/dist/creator2/core/replay-privacy-nodes.js +18 -0
  39. package/dist/creator2/core/replay-privacy-nodes.js.map +1 -0
  40. package/dist/creator2/core/replay-privacy.d.ts +15 -0
  41. package/dist/creator2/core/replay-privacy.d.ts.map +1 -0
  42. package/dist/creator2/core/replay-privacy.js +36 -0
  43. package/dist/creator2/core/replay-privacy.js.map +1 -0
  44. package/dist/creator2/core/replay.d.ts +102 -0
  45. package/dist/creator2/core/replay.d.ts.map +1 -0
  46. package/dist/creator2/core/replay.js +653 -0
  47. package/dist/creator2/core/replay.js.map +1 -0
  48. package/dist/creator2/core/transport.d.ts +3 -0
  49. package/dist/creator2/core/transport.d.ts.map +1 -0
  50. package/dist/creator2/core/transport.js +22 -0
  51. package/dist/creator2/core/transport.js.map +1 -0
  52. package/dist/creator2/core/types.d.ts +82 -0
  53. package/dist/creator2/core/types.d.ts.map +1 -0
  54. package/dist/creator2/core/types.js +3 -0
  55. package/dist/creator2/core/types.js.map +1 -0
  56. package/dist/creator2/core/validation.d.ts +13 -0
  57. package/dist/creator2/core/validation.d.ts.map +1 -0
  58. package/dist/creator2/core/validation.js +75 -0
  59. package/dist/creator2/core/validation.js.map +1 -0
  60. package/dist/creator2/core/version.d.ts +2 -0
  61. package/dist/creator2/core/version.d.ts.map +1 -0
  62. package/dist/creator2/core/version.js +5 -0
  63. package/dist/creator2/core/version.js.map +1 -0
  64. package/dist/creator2/creator2/capture.d.ts +17 -0
  65. package/dist/creator2/creator2/capture.d.ts.map +1 -0
  66. package/dist/creator2/creator2/capture.js +168 -0
  67. package/dist/creator2/creator2/capture.js.map +1 -0
  68. package/dist/creator2/creator2/index.d.ts +8 -0
  69. package/dist/creator2/creator2/index.d.ts.map +1 -0
  70. package/dist/creator2/creator2/index.js +19 -0
  71. package/dist/creator2/creator2/index.js.map +1 -0
  72. package/dist/creator2/creator2/pointer.d.ts +5 -0
  73. package/dist/creator2/creator2/pointer.d.ts.map +1 -0
  74. package/dist/creator2/creator2/pointer.js +49 -0
  75. package/dist/creator2/creator2/pointer.js.map +1 -0
  76. package/dist/creator2/creator2/transport.d.ts +8 -0
  77. package/dist/creator2/creator2/transport.d.ts.map +1 -0
  78. package/dist/creator2/creator2/transport.js +38 -0
  79. package/dist/creator2/creator2/transport.js.map +1 -0
  80. package/dist/creator3/core/client.d.ts +30 -0
  81. package/dist/creator3/core/client.d.ts.map +1 -0
  82. package/dist/creator3/core/client.js +97 -0
  83. package/dist/creator3/core/client.js.map +1 -0
  84. package/dist/creator3/core/replay-async-transport.d.ts +4 -0
  85. package/dist/creator3/core/replay-async-transport.d.ts.map +1 -0
  86. package/dist/creator3/core/replay-async-transport.js +16 -0
  87. package/dist/creator3/core/replay-async-transport.js.map +1 -0
  88. package/dist/creator3/core/replay-budget.d.ts +28 -0
  89. package/dist/creator3/core/replay-budget.d.ts.map +1 -0
  90. package/dist/creator3/core/replay-budget.js +118 -0
  91. package/dist/creator3/core/replay-budget.js.map +1 -0
  92. package/dist/creator3/core/replay-diagnostics.d.ts +36 -0
  93. package/dist/creator3/core/replay-diagnostics.d.ts.map +1 -0
  94. package/dist/creator3/core/replay-diagnostics.js +36 -0
  95. package/dist/creator3/core/replay-diagnostics.js.map +1 -0
  96. package/dist/creator3/core/replay-encoding.d.ts +31 -0
  97. package/dist/creator3/core/replay-encoding.d.ts.map +1 -0
  98. package/dist/creator3/core/replay-encoding.js +62 -0
  99. package/dist/creator3/core/replay-encoding.js.map +1 -0
  100. package/dist/creator3/core/replay-file.d.ts +5 -0
  101. package/dist/creator3/core/replay-file.d.ts.map +1 -0
  102. package/dist/creator3/core/replay-file.js +31 -0
  103. package/dist/creator3/core/replay-file.js.map +1 -0
  104. package/dist/creator3/core/replay-pixels.d.ts +3 -0
  105. package/dist/creator3/core/replay-pixels.d.ts.map +1 -0
  106. package/dist/creator3/core/replay-pixels.js +13 -0
  107. package/dist/creator3/core/replay-pixels.js.map +1 -0
  108. package/dist/creator3/core/replay-privacy-nodes.d.ts +14 -0
  109. package/dist/creator3/core/replay-privacy-nodes.d.ts.map +1 -0
  110. package/dist/creator3/core/replay-privacy-nodes.js +15 -0
  111. package/dist/creator3/core/replay-privacy-nodes.js.map +1 -0
  112. package/dist/creator3/core/replay-privacy.d.ts +15 -0
  113. package/dist/creator3/core/replay-privacy.d.ts.map +1 -0
  114. package/dist/creator3/core/replay-privacy.js +33 -0
  115. package/dist/creator3/core/replay-privacy.js.map +1 -0
  116. package/dist/creator3/core/replay.d.ts +102 -0
  117. package/dist/creator3/core/replay.d.ts.map +1 -0
  118. package/dist/creator3/core/replay.js +576 -0
  119. package/dist/creator3/core/replay.js.map +1 -0
  120. package/dist/creator3/core/transport.d.ts +3 -0
  121. package/dist/creator3/core/transport.d.ts.map +1 -0
  122. package/dist/creator3/core/transport.js +19 -0
  123. package/dist/creator3/core/transport.js.map +1 -0
  124. package/dist/creator3/core/types.d.ts +82 -0
  125. package/dist/creator3/core/types.d.ts.map +1 -0
  126. package/dist/creator3/core/types.js +2 -0
  127. package/dist/creator3/core/types.js.map +1 -0
  128. package/dist/creator3/core/validation.d.ts +13 -0
  129. package/dist/creator3/core/validation.d.ts.map +1 -0
  130. package/dist/creator3/core/validation.js +70 -0
  131. package/dist/creator3/core/validation.js.map +1 -0
  132. package/dist/creator3/core/version.d.ts +2 -0
  133. package/dist/creator3/core/version.d.ts.map +1 -0
  134. package/dist/creator3/core/version.js +2 -0
  135. package/dist/creator3/core/version.js.map +1 -0
  136. package/dist/creator3/creator3/capture.d.ts +17 -0
  137. package/dist/creator3/creator3/capture.d.ts.map +1 -0
  138. package/dist/creator3/creator3/capture.js +112 -0
  139. package/dist/creator3/creator3/capture.js.map +1 -0
  140. package/dist/creator3/creator3/index.d.ts +8 -0
  141. package/dist/creator3/creator3/index.d.ts.map +1 -0
  142. package/dist/creator3/creator3/index.js +14 -0
  143. package/dist/creator3/creator3/index.js.map +1 -0
  144. package/dist/creator3/creator3/pointer.d.ts +5 -0
  145. package/dist/creator3/creator3/pointer.d.ts.map +1 -0
  146. package/dist/creator3/creator3/pointer.js +29 -0
  147. package/dist/creator3/creator3/pointer.js.map +1 -0
  148. package/dist/creator3/creator3/replay-render-cycle.d.ts +9 -0
  149. package/dist/creator3/creator3/replay-render-cycle.d.ts.map +1 -0
  150. package/dist/creator3/creator3/replay-render-cycle.js +12 -0
  151. package/dist/creator3/creator3/replay-render-cycle.js.map +1 -0
  152. package/dist/creator3/creator3/transport.d.ts +9 -0
  153. package/dist/creator3/creator3/transport.d.ts.map +1 -0
  154. package/dist/creator3/creator3/transport.js +28 -0
  155. package/dist/creator3/creator3/transport.js.map +1 -0
  156. package/dist/creator3/package.json +1 -0
  157. package/native/android/src/main/java/com/ft/sdk/cocos/FTCocosReplayBridge.java +168 -0
  158. package/native/android/src/main/java/com/ft/sdk/cocos/FTCocosReplayImageJobs.java +55 -0
  159. package/native/ios/FTCocosReplayBridge.h +10 -0
  160. package/native/ios/FTCocosReplayBridge.m +275 -0
  161. package/native/ios/FTCocosReplayBridge.podspec +18 -0
  162. package/native/ios/FTCocosReplayImageJobs.h +11 -0
  163. package/native/ios/FTCocosReplayImageJobs.m +54 -0
  164. package/native/ios/Package.swift +26 -0
  165. package/native/ios/include/FTCocosReplayBridge.h +10 -0
  166. package/native/shared/FTReplayFileBridge.h +62 -0
  167. package/native/shared/FTReplayFileWorker.h +88 -0
  168. package/native-integration.json +9 -0
  169. package/package.json +58 -0
@@ -0,0 +1,168 @@
1
+ package com.ft.sdk.cocos;
2
+
3
+ import android.app.Activity;
4
+ import com.ft.sdk.FTSdk;
5
+ import com.ft.sdk.SessionReplayManager;
6
+ import com.ft.sdk.sessionreplay.FTSessionReplayConfig;
7
+ import com.ft.sdk.sessionreplay.SessionReplayInternalCallback;
8
+ import com.ft.sdk.sessionreplay.TouchPrivacy;
9
+ import org.json.JSONObject;
10
+ import java.io.ByteArrayOutputStream;
11
+ import java.io.File;
12
+ import java.io.FileInputStream;
13
+ import java.lang.reflect.InvocationTargetException;
14
+ import java.lang.reflect.Method;
15
+
16
+ /** Optional native Session Replay entry point. */
17
+ public final class FTCocosReplayBridge {
18
+ private FTCocosReplayBridge() {}
19
+ public static String invoke(String method, String payload) {
20
+ return FTBridgeTransport.invoke("session-replay", method, payload);
21
+ }
22
+
23
+ static String invokeLocal(String method, String payload) {
24
+ try {
25
+ Object value = dispatch(method, payload == null || payload.isEmpty()
26
+ ? new JSONObject() : new JSONObject(payload));
27
+ JSONObject response = new JSONObject();
28
+ response.put("ok", true);
29
+ if (value != null) response.put("value", JSONObject.wrap(value));
30
+ return response.toString();
31
+ } catch (Throwable error) {
32
+ JSONObject response = new JSONObject();
33
+ try {
34
+ response.put("ok", false);
35
+ response.put("error", error.getClass().getSimpleName() + ": " + error.getMessage());
36
+ } catch (Throwable ignored) {
37
+ return "{\"ok\":false,\"error\":\"Native bridge failure\"}";
38
+ }
39
+ return response.toString();
40
+ }
41
+ }
42
+
43
+ private static Object dispatch(String method, JSONObject payload) throws Exception {
44
+ switch (method) {
45
+ case "replay.capabilities":
46
+ FTSessionReplayConfig.class.getDeclaredMethod("setExternalRecorderMode", boolean.class);
47
+ if (payload.optBoolean("hybrid")) { ensureNativeHostInitialized(); hybridRecorderSwitchMethod(); }
48
+ return new JSONObject().put("protocol", 1);
49
+ case "replay.beginSaveImage":
50
+ return FTCocosReplayImageJobs.begin(payload);
51
+ case "replay.pollSaveImage":
52
+ return FTCocosReplayImageJobs.poll(payload.getString("job"));
53
+ case "hybrid.setExternalRecorderActive":
54
+ setExternalRecorderActive(payload.getBoolean("active"));
55
+ return null;
56
+ case "replay.configure":
57
+ configureReplay(payload);
58
+ return null;
59
+ case "replay.getContext":
60
+ return SessionReplayManager.get().getCurrentFlutterRumContext();
61
+ case "replay.saveImage":
62
+ return saveReplayImage(payload);
63
+ case "replay.saveImageV2":
64
+ return saveReplayImageV2(payload);
65
+ case "replay.writeSegment":
66
+ SessionReplayManager.get().writeExternalSegment(
67
+ payload.getString("segment"), payload.getString("viewId"));
68
+ return null;
69
+ case "replay.setRecordCount":
70
+ SessionReplayManager.get().setExternalRecordCount(
71
+ payload.getString("viewId"), payload.getLong("count"));
72
+ return null;
73
+ case "replay.stop":
74
+ return null;
75
+ default:
76
+ throw new IllegalArgumentException("Unknown bridge method: " + method);
77
+ }
78
+ }
79
+
80
+ private static void ensureNativeHostInitialized() {
81
+ if (FTSdk.get() == null) {
82
+ throw new IllegalStateException(
83
+ "The native host must install the native SDK before FTCocosSDK.attach()");
84
+ }
85
+ }
86
+
87
+ private static Method hybridRecorderSwitchMethod() {
88
+ try {
89
+ return SessionReplayManager.class.getMethod(
90
+ "setExternalRecorderActive", boolean.class);
91
+ } catch (NoSuchMethodException error) {
92
+ throw new IllegalStateException(
93
+ "Android Session Replay does not support Hybrid recorder switching; "
94
+ + "upgrade the Native SDK");
95
+ }
96
+ }
97
+
98
+ private static void setExternalRecorderActive(boolean active) throws Exception {
99
+ ensureNativeHostInitialized();
100
+ try {
101
+ hybridRecorderSwitchMethod().invoke(SessionReplayManager.get(), active);
102
+ } catch (InvocationTargetException error) {
103
+ Throwable cause = error.getCause();
104
+ if (cause instanceof Exception) throw (Exception) cause;
105
+ throw error;
106
+ }
107
+ }
108
+
109
+ private static void configureReplay(JSONObject json) throws Exception {
110
+ FTSessionReplayConfig config = new FTSessionReplayConfig();
111
+ if (json.has("sampleRate")) config.setSampleRate((float) json.getDouble("sampleRate"));
112
+ if (json.has("sessionOnErrorSampleRate")) config.setSessionReplayOnErrorSampleRate((float) json.getDouble("sessionOnErrorSampleRate"));
113
+ if ("show".equals(json.optString("touchPrivacy"))) config.setTouchPrivacy(TouchPrivacy.SHOW);
114
+ config.setInternalCallback(new SessionReplayInternalCallback() {
115
+ @Override public Activity getCurrentActivity() { return FTBridgeTransport.resolveActivity(); }
116
+ });
117
+ Method externalMode = FTSessionReplayConfig.class.getDeclaredMethod("setExternalRecorderMode", boolean.class);
118
+ externalMode.setAccessible(true);
119
+ externalMode.invoke(config, true);
120
+ FTSdk.initSessionReplayConfig(config);
121
+ }
122
+
123
+ private static String saveReplayImage(JSONObject json) throws Exception {
124
+ byte[] bytes = readFile(new File(json.getString("path")));
125
+ return SessionReplayManager.get().saveFlutterImageResource(bytes, json.getInt("width"), json.getInt("height"));
126
+ }
127
+
128
+ private static Object saveReplayImageV2(JSONObject json) throws Exception {
129
+ byte[] bytes = readFile(new File(json.getString("path")));
130
+ Method method;
131
+ try {
132
+ method = SessionReplayManager.class.getMethod(
133
+ "saveExternalImageResourceV2",
134
+ byte[].class,
135
+ int.class,
136
+ int.class,
137
+ float.class,
138
+ int.class);
139
+ } catch (NoSuchMethodException unavailable) {
140
+ throw new IllegalArgumentException("Unknown bridge method: replay.saveImageV2");
141
+ }
142
+ try {
143
+ return method.invoke(
144
+ SessionReplayManager.get(),
145
+ bytes,
146
+ json.getInt("width"),
147
+ json.getInt("height"),
148
+ (float) json.optDouble("quality", 0.45),
149
+ json.optInt("maxFrameBytes", 40 * 1024));
150
+ } catch (InvocationTargetException error) {
151
+ Throwable cause = error.getCause();
152
+ if (cause instanceof Exception) throw (Exception) cause;
153
+ throw error;
154
+ }
155
+ }
156
+
157
+ private static byte[] readFile(File file) throws Exception {
158
+ FileInputStream input = new FileInputStream(file);
159
+ try {
160
+ ByteArrayOutputStream output = new ByteArrayOutputStream((int) Math.min(file.length(), Integer.MAX_VALUE));
161
+ byte[] buffer = new byte[16 * 1024];
162
+ int read;
163
+ while ((read = input.read(buffer)) >= 0) output.write(buffer, 0, read);
164
+ return output.toByteArray();
165
+ } finally {
166
+ input.close();
167
+ }
168
+ }}
@@ -0,0 +1,55 @@
1
+ package com.ft.sdk.cocos;
2
+
3
+ import org.json.JSONObject;
4
+ import java.util.UUID;
5
+ import java.util.concurrent.ExecutorService;
6
+ import java.util.concurrent.Executors;
7
+
8
+ /** One image in flight across JS runtimes; the caller polls without waiting on disk/encoding. */
9
+ final class FTCocosReplayImageJobs {
10
+ private static final ExecutorService worker = Executors.newSingleThreadExecutor(runnable -> {
11
+ Thread thread = new Thread(runnable, "cocos-replay-image");
12
+ thread.setDaemon(true);
13
+ return thread;
14
+ });
15
+ private static Job current;
16
+
17
+ private static final class Job {
18
+ final String id = UUID.randomUUID().toString();
19
+ String response;
20
+ }
21
+
22
+ static synchronized String begin(JSONObject arguments) throws Exception {
23
+ final String method = arguments.getString("method");
24
+ if (!"replay.saveImage".equals(method) && !"replay.saveImageV2".equals(method)) {
25
+ throw new IllegalArgumentException("Unsupported asynchronous Replay method");
26
+ }
27
+ final String payload = arguments.getJSONObject("arguments").toString();
28
+ if (current != null && current.response == null) throw new IllegalStateException("Replay encoder is busy");
29
+ final Job job = new Job();
30
+ current = job;
31
+ try {
32
+ worker.execute(() -> {
33
+ String response = FTCocosReplayBridge.invokeLocal(method, payload);
34
+ synchronized (FTCocosReplayImageJobs.class) {
35
+ job.response = response;
36
+ }
37
+ });
38
+ } catch (RuntimeException error) {
39
+ current = null;
40
+ throw error;
41
+ }
42
+ return job.id;
43
+ }
44
+
45
+ static synchronized JSONObject poll(String id) throws Exception {
46
+ if (current == null || !current.id.equals(id)) throw new IllegalArgumentException("Replay encoding job is unavailable");
47
+ JSONObject result = new JSONObject();
48
+ result.put("pending", current.response == null);
49
+ if (current.response != null) {
50
+ result.put("response", current.response);
51
+ current = null;
52
+ }
53
+ return result;
54
+ }
55
+ }
@@ -0,0 +1,10 @@
1
+ #import <Foundation/Foundation.h>
2
+
3
+ NS_ASSUME_NONNULL_BEGIN
4
+
5
+ /** Cocos JSB reflection entry point. */
6
+ @interface FTCocosReplayBridge : NSObject
7
+ + (NSString *)invoke:(NSString *)method payload:(NSString *)payload;
8
+ @end
9
+
10
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,275 @@
1
+ #import "FTCocosReplayBridge.h"
2
+ #import "FTCocosReplayImageJobs.h"
3
+ #import "FTMobileAgent.h"
4
+ #import "FTSessionReplay.h"
5
+ #import <UIKit/UIKit.h>
6
+ #import <objc/message.h>
7
+
8
+ @implementation FTCocosReplayBridge
9
+
10
+ + (NSString *)invoke:(NSString *)method payload:(NSString *)payload {
11
+ @try {
12
+ NSData *data = [payload dataUsingEncoding:NSUTF8StringEncoding];
13
+ NSDictionary *arguments = data.length > 0
14
+ ? [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]
15
+ : @{};
16
+ if (![arguments isKindOfClass:NSDictionary.class]) {
17
+ return [self responseWithValue:nil error:@"Payload must be a JSON object"];
18
+ }
19
+ id value = [self dispatch:method arguments:arguments];
20
+ return [self responseWithValue:value error:nil];
21
+ } @catch (NSException *exception) {
22
+ return [self responseWithValue:nil error:[NSString stringWithFormat:@"%@: %@", exception.name, exception.reason ?: @"unknown error"]];
23
+ }
24
+ }
25
+
26
+ + (id)dispatch:(NSString *)method arguments:(NSDictionary *)arguments {
27
+ if ([method isEqualToString:@"replay.capabilities"]) {
28
+ [self ensureExternalReplayAPIForConfig:[[FTSessionReplayConfig alloc] init]];
29
+ if ([arguments[@"hybrid"] boolValue]) { [self ensureNativeHostInitialized]; [self ensureHybridRecorderSwitch]; }
30
+ return @{ @"protocol": @1 };
31
+ }
32
+ if ([method isEqualToString:@"replay.beginSaveImage"]) return [self beginSaveImage:arguments];
33
+ if ([method isEqualToString:@"replay.pollSaveImage"]) {
34
+ return [FTCocosReplayImageJobs poll:[self requiredText:arguments key:@"job"]];
35
+ }
36
+ if ([method isEqualToString:@"hybrid.setExternalRecorderActive"]) {
37
+ [self ensureNativeHostInitialized];
38
+ [self setExternalRecorderActive:[arguments[@"active"] boolValue]];
39
+ return nil;
40
+ }
41
+ if ([method isEqualToString:@"replay.configure"]) {
42
+ FTSessionReplayConfig *config = [[FTSessionReplayConfig alloc] init];
43
+ [self ensureExternalReplayAPIForConfig:config];
44
+ if (arguments[@"sampleRate"]) config.sampleRate = [self percent:arguments[@"sampleRate"]];
45
+ if (arguments[@"sessionOnErrorSampleRate"]) config.sessionReplayOnErrorSampleRate = [self percent:arguments[@"sessionOnErrorSampleRate"]];
46
+ if ([[self text:arguments[@"touchPrivacy"]] isEqualToString:@"show"]) config.touchPrivacy = FTTouchPrivacyLevelShow;
47
+ SEL externalMode = NSSelectorFromString(@"setExternalRecorderMode:");
48
+ ((void (*)(id, SEL, BOOL))objc_msgSend)(config, externalMode, YES);
49
+ [[FTRumSessionReplay sharedInstance] startWithSessionReplayConfig:config];
50
+ return nil;
51
+ }
52
+ if ([method isEqualToString:@"replay.getContext"]) {
53
+ return [self invokeReplayObjectSelector:NSSelectorFromString(@"currentExternalRUMContext")];
54
+ }
55
+ if ([method isEqualToString:@"replay.saveImage"]) {
56
+ NSData *rgba = [NSData dataWithContentsOfFile:[self requiredText:arguments key:@"path"]];
57
+ if (!rgba) [self fail:@"Unable to read replay RGBA file"];
58
+ NSData *png = [self pngDataFromRGBA:rgba
59
+ width:[arguments[@"width"] unsignedIntegerValue]
60
+ height:[arguments[@"height"] unsignedIntegerValue]];
61
+ if (!png) [self fail:@"Unable to encode replay image"];
62
+ return [self invokeReplayObjectSelector:NSSelectorFromString(@"saveExternalImageResourceData:mimeType:")
63
+ first:png
64
+ second:@"image/png"];
65
+ }
66
+ if ([method isEqualToString:@"replay.saveImageV2"]) {
67
+ NSUInteger width = [arguments[@"width"] unsignedIntegerValue];
68
+ NSUInteger height = [arguments[@"height"] unsignedIntegerValue];
69
+ NSData *rgba = [NSData dataWithContentsOfFile:[self requiredText:arguments key:@"path"]];
70
+ if (!rgba) [self fail:@"Unable to read replay RGBA file"];
71
+ UIImage *image = [self imageFromRGBA:rgba width:width height:height];
72
+ if (!image) [self fail:@"Unable to decode replay RGBA image"];
73
+
74
+ CGFloat quality = MIN(1.0, MAX(0.1, [arguments[@"quality"] doubleValue] ?: 0.45));
75
+ NSUInteger maxFrameBytes = [arguments[@"maxFrameBytes"] unsignedIntegerValue] ?: 40 * 1024;
76
+ UIImage *encodedImage = [self opaqueImageFromImage:image size:CGSizeMake(width, height)];
77
+ NSData *jpeg = UIImageJPEGRepresentation(encodedImage, quality);
78
+ if (jpeg.length > maxFrameBytes) {
79
+ quality = MAX(0.2, quality * 0.7);
80
+ jpeg = UIImageJPEGRepresentation(encodedImage, quality);
81
+ }
82
+ if (jpeg.length > maxFrameBytes && width > 1 && height > 1) {
83
+ width = MAX(1, (NSUInteger)floor(width * 0.75));
84
+ height = MAX(1, (NSUInteger)floor(height * 0.75));
85
+ encodedImage = [self opaqueImageFromImage:image size:CGSizeMake(width, height)];
86
+ jpeg = UIImageJPEGRepresentation(encodedImage, quality);
87
+ }
88
+ if (!jpeg || jpeg.length > maxFrameBytes) return @{ @"accepted": @NO };
89
+
90
+ NSString *resourceID = [self invokeReplayObjectSelector:NSSelectorFromString(@"saveExternalImageResourceData:mimeType:")
91
+ first:jpeg
92
+ second:@"image/jpeg"];
93
+ if (resourceID.length == 0) [self fail:@"Unable to save replay image resource"];
94
+ return @{
95
+ @"accepted": @YES,
96
+ @"resourceId": resourceID,
97
+ @"byteSize": @(jpeg.length),
98
+ @"width": @(width),
99
+ @"height": @(height),
100
+ @"mimeType": @"image/jpeg",
101
+ };
102
+ }
103
+ if ([method isEqualToString:@"replay.writeSegment"]) {
104
+ [self invokeReplayVoidSelector:NSSelectorFromString(@"writeExternalSegment:viewID:")
105
+ first:[self requiredText:arguments key:@"segment"]
106
+ second:[self requiredText:arguments key:@"viewId"]];
107
+ return nil;
108
+ }
109
+ if ([method isEqualToString:@"replay.setRecordCount"]) {
110
+ [self invokeReplayCountSelector:NSSelectorFromString(@"setExternalRecordCountForViewID:count:")
111
+ viewID:[self requiredText:arguments key:@"viewId"]
112
+ count:[arguments[@"count"] unsignedIntegerValue]];
113
+ return nil;
114
+ }
115
+ if ([method isEqualToString:@"replay.stop"]) return nil;
116
+ [self fail:[NSString stringWithFormat:@"Unknown bridge method: %@", method]];
117
+ return nil;
118
+ }
119
+
120
+ + (NSString *)beginSaveImage:(NSDictionary *)arguments {
121
+ NSString *method = [self requiredText:arguments key:@"method"];
122
+ if (![method isEqualToString:@"replay.saveImage"] && ![method isEqualToString:@"replay.saveImageV2"]) {
123
+ [self fail:@"Unsupported asynchronous Replay method"];
124
+ }
125
+ NSDictionary *payload = [[self dictionary:arguments[@"arguments"]] copy];
126
+ if (!payload) [self fail:@"Replay image arguments are required"];
127
+ return [FTCocosReplayImageJobs beginWithWork:^NSString *{
128
+ return [self responseWithValue:[self dispatch:method arguments:payload] error:nil];
129
+ }];
130
+ }
131
+
132
+ + (void)ensureHybridRecorderSwitch {
133
+ SEL selector = NSSelectorFromString(@"setExternalRecorderActive:");
134
+ if (![[FTRumSessionReplay sharedInstance] respondsToSelector:selector]) {
135
+ [self fail:@"iOS Session Replay does not support Hybrid recorder switching; upgrade the Native SDK"];
136
+ }
137
+ }
138
+
139
+ + (void)setExternalRecorderActive:(BOOL)active {
140
+ [self ensureHybridRecorderSwitch];
141
+ SEL selector = NSSelectorFromString(@"setExternalRecorderActive:");
142
+ ((void (*)(id, SEL, BOOL))objc_msgSend)([FTRumSessionReplay sharedInstance], selector, active);
143
+ }
144
+
145
+ + (id)invokeReplayObjectSelector:(SEL)selector {
146
+ id target = [FTRumSessionReplay sharedInstance];
147
+ if (![target respondsToSelector:selector]) [self missingReplayAPI:selector];
148
+ return ((id (*)(id, SEL))objc_msgSend)(target, selector);
149
+ }
150
+
151
+ + (id)invokeReplayObjectSelector:(SEL)selector object:(id)object {
152
+ id target = [FTRumSessionReplay sharedInstance];
153
+ if (![target respondsToSelector:selector]) [self missingReplayAPI:selector];
154
+ return ((id (*)(id, SEL, id))objc_msgSend)(target, selector, object);
155
+ }
156
+
157
+ + (id)invokeReplayObjectSelector:(SEL)selector first:(id)first second:(id)second {
158
+ id target = [FTRumSessionReplay sharedInstance];
159
+ if (![target respondsToSelector:selector]) [self missingReplayAPI:selector];
160
+ return ((id (*)(id, SEL, id, id))objc_msgSend)(target, selector, first, second);
161
+ }
162
+
163
+ + (void)invokeReplayVoidSelector:(SEL)selector first:(id)first second:(id)second {
164
+ id target = [FTRumSessionReplay sharedInstance];
165
+ if (![target respondsToSelector:selector]) [self missingReplayAPI:selector];
166
+ ((void (*)(id, SEL, id, id))objc_msgSend)(target, selector, first, second);
167
+ }
168
+
169
+ + (void)invokeReplayCountSelector:(SEL)selector viewID:(NSString *)viewID count:(NSUInteger)count {
170
+ id target = [FTRumSessionReplay sharedInstance];
171
+ if (![target respondsToSelector:selector]) [self missingReplayAPI:selector];
172
+ ((void (*)(id, SEL, id, NSUInteger))objc_msgSend)(target, selector, viewID, count);
173
+ }
174
+
175
+ + (void)missingReplayAPI:(SEL)selector {
176
+ [self fail:[NSString stringWithFormat:@"FTMobileSDK lacks external Session Replay API %@; use the Cocos-compatible iOS SDK release", NSStringFromSelector(selector)]];
177
+ }
178
+
179
+ + (void)ensureExternalReplayAPIForConfig:(FTSessionReplayConfig *)config {
180
+ SEL externalMode = NSSelectorFromString(@"setExternalRecorderMode:");
181
+ if (![config respondsToSelector:externalMode]) [self missingReplayAPI:externalMode];
182
+
183
+ id target = [FTRumSessionReplay sharedInstance];
184
+ NSArray<NSString *> *selectorNames = @[
185
+ @"currentExternalRUMContext",
186
+ @"saveExternalImageResourceData:mimeType:",
187
+ @"writeExternalSegment:viewID:",
188
+ @"setExternalRecordCountForViewID:count:",
189
+ ];
190
+ for (NSString *name in selectorNames) {
191
+ SEL selector = NSSelectorFromString(name);
192
+ if (![target respondsToSelector:selector]) [self missingReplayAPI:selector];
193
+ }
194
+ }
195
+
196
+ + (NSData *)pngDataFromRGBA:(NSData *)rgba width:(NSUInteger)width height:(NSUInteger)height {
197
+ UIImage *image = [self imageFromRGBA:rgba width:width height:height];
198
+ return image ? UIImagePNGRepresentation(image) : nil;
199
+ }
200
+
201
+ + (UIImage *)imageFromRGBA:(NSData *)rgba width:(NSUInteger)width height:(NSUInteger)height {
202
+ if (width == 0 || height == 0 || width > NSUIntegerMax / height / 4 || rgba.length < width * height * 4) {
203
+ return nil;
204
+ }
205
+ NSMutableData *pixels = [rgba mutableCopy];
206
+ CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
207
+ CGContextRef context = CGBitmapContextCreate(pixels.mutableBytes,
208
+ width,
209
+ height,
210
+ 8,
211
+ width * 4,
212
+ colorSpace,
213
+ (CGBitmapInfo)kCGBitmapByteOrderDefault |
214
+ (CGBitmapInfo)kCGImageAlphaPremultipliedLast);
215
+ CGColorSpaceRelease(colorSpace);
216
+ if (!context) return nil;
217
+ CGImageRef imageRef = CGBitmapContextCreateImage(context);
218
+ CGContextRelease(context);
219
+ if (!imageRef) return nil;
220
+ UIImage *image = [UIImage imageWithCGImage:imageRef];
221
+ CGImageRelease(imageRef);
222
+ return image;
223
+ }
224
+
225
+ + (UIImage *)opaqueImageFromImage:(UIImage *)image size:(CGSize)size {
226
+ UIGraphicsBeginImageContextWithOptions(size, YES, 1.0);
227
+ [[UIColor whiteColor] setFill];
228
+ UIRectFill(CGRectMake(0, 0, size.width, size.height));
229
+ [image drawInRect:CGRectMake(0, 0, size.width, size.height)];
230
+ UIImage *opaque = UIGraphicsGetImageFromCurrentImageContext();
231
+ UIGraphicsEndImageContext();
232
+ return opaque;
233
+ }
234
+
235
+ + (void)ensureNativeHostInitialized {
236
+ @try {
237
+ if (![FTMobileAgent sharedInstance]) {
238
+ [self fail:@"The native host must initialize the native SDK before FTCocosSDK.attach()"];
239
+ }
240
+ } @catch (__unused NSException *exception) {
241
+ [self fail:@"The native host must initialize the native SDK before FTCocosSDK.attach()"];
242
+ }
243
+ }
244
+
245
+ + (NSString *)responseWithValue:(id)value error:(NSString *)error {
246
+ NSMutableDictionary *response = [@{ @"ok": @(error == nil) } mutableCopy];
247
+ if (value) response[@"value"] = value;
248
+ if (error) response[@"error"] = error;
249
+ NSData *data = [NSJSONSerialization dataWithJSONObject:response options:0 error:nil];
250
+ return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] ?: @"{\"ok\":false}";
251
+ }
252
+
253
+ + (NSDictionary *)dictionary:(id)value {
254
+ return [value isKindOfClass:NSDictionary.class] ? value : nil;
255
+ }
256
+
257
+ + (NSString *)text:(id)value {
258
+ return [value isKindOfClass:NSString.class] && [value length] > 0 ? value : nil;
259
+ }
260
+
261
+ + (NSString *)requiredText:(NSDictionary *)arguments key:(NSString *)key {
262
+ NSString *value = [self text:arguments[key]];
263
+ if (value.length == 0) [self fail:[NSString stringWithFormat:@"%@ is required", key]];
264
+ return value;
265
+ }
266
+
267
+ + (int)percent:(NSNumber *)number {
268
+ return (int)lround(MIN(1.0, MAX(0.0, number.doubleValue)) * 100.0);
269
+ }
270
+
271
+ + (void)fail:(NSString *)message {
272
+ @throw [NSException exceptionWithName:@"FTCocosBridgeError" reason:message userInfo:nil];
273
+ }
274
+
275
+ @end
@@ -0,0 +1,18 @@
1
+ Pod::Spec.new do |s|
2
+ s.name = 'FTCocosReplayBridge'
3
+ s.version = '0.1.0-alpha.6'
4
+ s.summary = 'CloudCare Mobile SDK native bridge for Cocos Creator.'
5
+ s.homepage = 'https://github.com/GuanceCloud/datakit-cocos'
6
+ s.license = { :type => 'Apache-2.0' }
7
+ s.author = { 'Guance Cloud' => 'support@guance.com' }
8
+ s.source = { :path => '.' }
9
+ s.platform = :ios, '12.0'
10
+ s.source_files = 'FTCocosReplayBridge.{h,m}', 'FTCocosReplayImageJobs.{h,m}'
11
+ s.public_header_files = 'FTCocosReplayBridge.h'
12
+ s.requires_arc = true
13
+ # Cocos resolves FTCocosReplayBridge by class name at runtime, so no direct symbol
14
+ # reference would otherwise keep the Objective-C category/class in the app.
15
+ s.user_target_xcconfig = { 'OTHER_LDFLAGS' => '$(inherited) -ObjC' }
16
+ s.dependency 'GuanceSDK/FTSessionReplay', '1.6.8-alpha.5'
17
+ s.dependency 'GuanceSDK/Agent', '1.6.8-alpha.5'
18
+ end
@@ -0,0 +1,11 @@
1
+ #import <Foundation/Foundation.h>
2
+
3
+ NS_ASSUME_NONNULL_BEGIN
4
+
5
+ /** One background image operation, with nonblocking result retrieval from JSB. */
6
+ @interface FTCocosReplayImageJobs : NSObject
7
+ + (NSString *)beginWithWork:(NSString *(^)(void))work;
8
+ + (NSDictionary *)poll:(NSString *)identifier;
9
+ @end
10
+
11
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,54 @@
1
+ #import "FTCocosReplayImageJobs.h"
2
+
3
+ @interface FTCocosReplayImageJob : NSObject
4
+ @property (nonatomic, copy) NSString *identifier;
5
+ @property (nonatomic, copy) NSString *response;
6
+ @end
7
+ @implementation FTCocosReplayImageJob
8
+ @end
9
+
10
+ static FTCocosReplayImageJob *currentJob;
11
+
12
+ @implementation FTCocosReplayImageJobs
13
+
14
+ + (NSString *)beginWithWork:(NSString *(^)(void))work {
15
+ FTCocosReplayImageJob *job = [FTCocosReplayImageJob new];
16
+ job.identifier = NSUUID.UUID.UUIDString;
17
+ @synchronized (self) {
18
+ if (currentJob && !currentJob.response) {
19
+ [NSException raise:NSInternalInconsistencyException format:@"Replay encoder is busy"];
20
+ }
21
+ currentJob = job;
22
+ }
23
+ dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
24
+ @autoreleasepool {
25
+ NSString *response;
26
+ @try {
27
+ response = work();
28
+ } @catch (NSException *exception) {
29
+ NSData *data = [NSJSONSerialization dataWithJSONObject:@{
30
+ @"ok": @NO, @"error": exception.reason ?: @"Replay encoding failed"
31
+ } options:0 error:nil];
32
+ response = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
33
+ }
34
+ @synchronized (self) {
35
+ job.response = response ?: @"{\"ok\":false,\"error\":\"Empty Replay encoding result\"}";
36
+ }
37
+ }
38
+ });
39
+ return job.identifier;
40
+ }
41
+
42
+ + (NSDictionary *)poll:(NSString *)identifier {
43
+ @synchronized (self) {
44
+ if (![currentJob.identifier isEqualToString:identifier]) {
45
+ [NSException raise:NSInvalidArgumentException format:@"Replay encoding job is unavailable"];
46
+ }
47
+ if (!currentJob.response) return @{ @"pending": @YES };
48
+ NSDictionary *result = @{ @"pending": @NO, @"response": currentJob.response };
49
+ currentJob = nil;
50
+ return result;
51
+ }
52
+ }
53
+
54
+ @end
@@ -0,0 +1,26 @@
1
+ // swift-tools-version:5.3
2
+ import PackageDescription
3
+
4
+ let package = Package(
5
+ name: "FTCocosReplayBridge",
6
+ platforms: [.iOS(.v12)],
7
+ products: [.library(name: "FTCocosReplayBridge", targets: ["FTCocosReplayBridge"])],
8
+ dependencies: [
9
+ .package(url: "https://github.com/GuanceCloud/datakit-ios.git", .exact("1.6.8-alpha.5")),
10
+ ],
11
+ targets: [
12
+ .target(
13
+ name: "FTCocosReplayBridge",
14
+ dependencies: [
15
+ .product(name: "GuanceSDK", package: "datakit-ios"),
16
+ .product(name: "GuanceSessionReplay", package: "datakit-ios"),
17
+ ],
18
+ path: ".",
19
+ exclude: ["FTCocosReplayBridge.podspec"],
20
+ sources: ["FTCocosReplayBridge.m", "FTCocosReplayImageJobs.m"],
21
+ publicHeadersPath: "include",
22
+ cSettings: [.headerSearchPath(".")],
23
+ linkerSettings: [.linkedFramework("UIKit")]
24
+ ),
25
+ ]
26
+ )
@@ -0,0 +1,10 @@
1
+ #import <Foundation/Foundation.h>
2
+
3
+ NS_ASSUME_NONNULL_BEGIN
4
+
5
+ /** Cocos JSB reflection entry point. */
6
+ @interface FTCocosReplayBridge : NSObject
7
+ + (NSString *)invoke:(NSString *)method payload:(NSString *)payload;
8
+ @end
9
+
10
+ NS_ASSUME_NONNULL_END