@bota.dev/react-native-app-sdk 2.0.0-beta.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/BotaDeviceSDK.podspec +55 -0
- package/LICENSE +21 -0
- package/README.md +101 -0
- package/android/build.gradle +76 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidDevices.kt +172 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidLifecycle.kt +100 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidLogs.kt +82 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidModels.kt +476 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidOTA.kt +58 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordingUploads.kt +180 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordings.kt +751 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidSecurity.kt +509 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidWiFi.kt +125 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKEncryptedUploadV2Materials.kt +46 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKModule.kt +723 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKPackage.kt +33 -0
- package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidDevicesTest.kt +211 -0
- package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidLifecycleTest.kt +132 -0
- package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidLogsTest.kt +57 -0
- package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidOTATest.kt +75 -0
- package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordingUploadsTest.kt +147 -0
- package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordingsTest.kt +355 -0
- package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidSecurityTest.kt +523 -0
- package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidWiFiTest.kt +100 -0
- package/generated/codegen-contract.json +3593 -0
- package/ios/BotaDeviceSDK.mm +1300 -0
- package/ios/BotaDeviceSDKAppleBridge.swift +2260 -0
- package/ios/BotaDeviceSDKAppleDevices.swift +191 -0
- package/ios/BotaDeviceSDKAppleLifecycle.swift +167 -0
- package/ios/BotaDeviceSDKAppleLogs.swift +79 -0
- package/ios/BotaDeviceSDKAppleOTA.swift +73 -0
- package/ios/BotaDeviceSDKAppleRecordingUploads.swift +178 -0
- package/ios/BotaDeviceSDKAppleRecordings.swift +670 -0
- package/ios/BotaDeviceSDKAppleSecurity.swift +586 -0
- package/ios/BotaDeviceSDKAppleWiFi.swift +128 -0
- package/ios/BotaDeviceSDKEncryptedUploadV2Materials.swift +99 -0
- package/lib/commonjs/BotaClient.js +194 -0
- package/lib/commonjs/BotaClient.js.map +1 -0
- package/lib/commonjs/ble/constants.js +10 -0
- package/lib/commonjs/ble/constants.js.map +1 -0
- package/lib/commonjs/ble/deviceLogs.js +50 -0
- package/lib/commonjs/ble/deviceLogs.js.map +1 -0
- package/lib/commonjs/cache/DeviceStateCache.js +6 -0
- package/lib/commonjs/cache/DeviceStateCache.js.map +1 -0
- package/lib/commonjs/client.js +746 -0
- package/lib/commonjs/client.js.map +1 -0
- package/lib/commonjs/compatibility/runtime.js +44 -0
- package/lib/commonjs/compatibility/runtime.js.map +1 -0
- package/lib/commonjs/index.js +123 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/managers/DeviceManager.js +647 -0
- package/lib/commonjs/managers/DeviceManager.js.map +1 -0
- package/lib/commonjs/managers/OTAManager.js +140 -0
- package/lib/commonjs/managers/OTAManager.js.map +1 -0
- package/lib/commonjs/managers/RecordingManager.js +444 -0
- package/lib/commonjs/managers/RecordingManager.js.map +1 -0
- package/lib/commonjs/managers/StreamingSession.js +175 -0
- package/lib/commonjs/managers/StreamingSession.js.map +1 -0
- package/lib/commonjs/managers/types.js +6 -0
- package/lib/commonjs/managers/types.js.map +1 -0
- package/lib/commonjs/models/Device.js +2 -0
- package/lib/commonjs/models/Device.js.map +1 -0
- package/lib/commonjs/models/Recording.js +2 -0
- package/lib/commonjs/models/Recording.js.map +1 -0
- package/lib/commonjs/models/Status.js +6 -0
- package/lib/commonjs/models/Status.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/specs/NativeBotaDeviceSDK.js +9 -0
- package/lib/commonjs/specs/NativeBotaDeviceSDK.js.map +1 -0
- package/lib/commonjs/sync/syncStatus.js +178 -0
- package/lib/commonjs/sync/syncStatus.js.map +1 -0
- package/lib/commonjs/utils/errors.js +194 -0
- package/lib/commonjs/utils/errors.js.map +1 -0
- package/lib/commonjs/utils/logger.js +84 -0
- package/lib/commonjs/utils/logger.js.map +1 -0
- package/lib/module/BotaClient.js +189 -0
- package/lib/module/BotaClient.js.map +1 -0
- package/lib/module/ble/constants.js +6 -0
- package/lib/module/ble/constants.js.map +1 -0
- package/lib/module/ble/deviceLogs.js +45 -0
- package/lib/module/ble/deviceLogs.js.map +1 -0
- package/lib/module/cache/DeviceStateCache.js +4 -0
- package/lib/module/cache/DeviceStateCache.js.map +1 -0
- package/lib/module/client.js +740 -0
- package/lib/module/client.js.map +1 -0
- package/lib/module/compatibility/runtime.js +35 -0
- package/lib/module/compatibility/runtime.js.map +1 -0
- package/lib/module/index.js +17 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/managers/DeviceManager.js +641 -0
- package/lib/module/managers/DeviceManager.js.map +1 -0
- package/lib/module/managers/OTAManager.js +134 -0
- package/lib/module/managers/OTAManager.js.map +1 -0
- package/lib/module/managers/RecordingManager.js +438 -0
- package/lib/module/managers/RecordingManager.js.map +1 -0
- package/lib/module/managers/StreamingSession.js +168 -0
- package/lib/module/managers/StreamingSession.js.map +1 -0
- package/lib/module/managers/types.js +4 -0
- package/lib/module/managers/types.js.map +1 -0
- package/lib/module/models/Device.js +2 -0
- package/lib/module/models/Device.js.map +1 -0
- package/lib/module/models/Recording.js +2 -0
- package/lib/module/models/Recording.js.map +1 -0
- package/lib/module/models/Status.js +4 -0
- package/lib/module/models/Status.js.map +1 -0
- package/lib/module/specs/NativeBotaDeviceSDK.js +5 -0
- package/lib/module/specs/NativeBotaDeviceSDK.js.map +1 -0
- package/lib/module/sync/syncStatus.js +174 -0
- package/lib/module/sync/syncStatus.js.map +1 -0
- package/lib/module/utils/errors.js +183 -0
- package/lib/module/utils/errors.js.map +1 -0
- package/lib/module/utils/logger.js +80 -0
- package/lib/module/utils/logger.js.map +1 -0
- package/lib/typescript/BotaClient.d.ts +40 -0
- package/lib/typescript/BotaClient.d.ts.map +1 -0
- package/lib/typescript/ble/constants.d.ts +4 -0
- package/lib/typescript/ble/constants.d.ts.map +1 -0
- package/lib/typescript/ble/deviceLogs.d.ts +10 -0
- package/lib/typescript/ble/deviceLogs.d.ts.map +1 -0
- package/lib/typescript/cache/DeviceStateCache.d.ts +14 -0
- package/lib/typescript/cache/DeviceStateCache.d.ts.map +1 -0
- package/lib/typescript/client.d.ts +208 -0
- package/lib/typescript/client.d.ts.map +1 -0
- package/lib/typescript/compatibility/runtime.d.ts +11 -0
- package/lib/typescript/compatibility/runtime.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +19 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/managers/DeviceManager.d.ts +117 -0
- package/lib/typescript/managers/DeviceManager.d.ts.map +1 -0
- package/lib/typescript/managers/OTAManager.d.ts +25 -0
- package/lib/typescript/managers/OTAManager.d.ts.map +1 -0
- package/lib/typescript/managers/RecordingManager.d.ts +54 -0
- package/lib/typescript/managers/RecordingManager.d.ts.map +1 -0
- package/lib/typescript/managers/StreamingSession.d.ts +35 -0
- package/lib/typescript/managers/StreamingSession.d.ts.map +1 -0
- package/lib/typescript/managers/types.d.ts +19 -0
- package/lib/typescript/managers/types.d.ts.map +1 -0
- package/lib/typescript/models/Device.d.ts +392 -0
- package/lib/typescript/models/Device.d.ts.map +1 -0
- package/lib/typescript/models/Recording.d.ts +259 -0
- package/lib/typescript/models/Recording.d.ts.map +1 -0
- package/lib/typescript/models/Status.d.ts +106 -0
- package/lib/typescript/models/Status.d.ts.map +1 -0
- package/lib/typescript/specs/NativeBotaDeviceSDK.d.ts +393 -0
- package/lib/typescript/specs/NativeBotaDeviceSDK.d.ts.map +1 -0
- package/lib/typescript/sync/syncStatus.d.ts +123 -0
- package/lib/typescript/sync/syncStatus.d.ts.map +1 -0
- package/lib/typescript/utils/errors.d.ts +83 -0
- package/lib/typescript/utils/errors.d.ts.map +1 -0
- package/lib/typescript/utils/logger.d.ts +36 -0
- package/lib/typescript/utils/logger.d.ts.map +1 -0
- package/package.json +100 -0
- package/scripts/bota_device_sdk_spm_workaround.rb +53 -0
- package/src/BotaClient.ts +231 -0
- package/src/ble/constants.ts +3 -0
- package/src/ble/deviceLogs.ts +61 -0
- package/src/cache/DeviceStateCache.ts +20 -0
- package/src/client.ts +1410 -0
- package/src/compatibility/runtime.ts +55 -0
- package/src/index.ts +146 -0
- package/src/managers/DeviceManager.ts +927 -0
- package/src/managers/OTAManager.ts +195 -0
- package/src/managers/RecordingManager.ts +578 -0
- package/src/managers/StreamingSession.ts +212 -0
- package/src/managers/types.ts +36 -0
- package/src/models/Device.ts +484 -0
- package/src/models/Recording.ts +281 -0
- package/src/models/Status.ts +128 -0
- package/src/specs/NativeBotaDeviceSDK.ts +542 -0
- package/src/sync/syncStatus.ts +243 -0
- package/src/utils/errors.ts +327 -0
- package/src/utils/logger.ts +122 -0
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
import BotaAppSDK
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
struct BotaDeviceSDKAppleRecordingFile: Equatable, Sendable {
|
|
5
|
+
let localPath: String
|
|
6
|
+
let isE2EEncrypted: Bool
|
|
7
|
+
let contentSHA256Hex: String?
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
protocol BotaDeviceSDKAppleRecordingClient: Sendable {
|
|
11
|
+
func listRecordings(_ device: ConnectedDevice) async throws -> [DeviceRecording]
|
|
12
|
+
func syncRecording(
|
|
13
|
+
_ device: ConnectedDevice,
|
|
14
|
+
recording: DeviceRecording,
|
|
15
|
+
sinkID: String
|
|
16
|
+
) async throws -> AsyncThrowingStream<RecordingSyncEvent, Error>
|
|
17
|
+
func transferMetadata(sinkID: String) async -> RecordingTransferMetadata?
|
|
18
|
+
func syncEncryptedRecordingV2(
|
|
19
|
+
_ device: ConnectedDevice,
|
|
20
|
+
recording: EncryptedUploadV2Recording,
|
|
21
|
+
provider: @escaping EncryptedUploadV2ProfileProvider
|
|
22
|
+
) async throws
|
|
23
|
+
func confirmRecording(_ device: ConnectedDevice, recordingUUID: String) async throws
|
|
24
|
+
func observeUploadOwnership(
|
|
25
|
+
_ device: ConnectedDevice,
|
|
26
|
+
recordingUUID: String,
|
|
27
|
+
uploadID: String,
|
|
28
|
+
destinationID: String
|
|
29
|
+
) async throws -> AsyncThrowingStream<UploadOwnershipEvent, Error>
|
|
30
|
+
func streamRecording(
|
|
31
|
+
_ device: ConnectedDevice,
|
|
32
|
+
recordingUUID: String,
|
|
33
|
+
sinkID: String,
|
|
34
|
+
chunkSizeBytes: Int,
|
|
35
|
+
flushIntervalMilliseconds: UInt64,
|
|
36
|
+
destinationProvider: @escaping StreamingChunkDestinationProvider,
|
|
37
|
+
finalize: @escaping StreamingFinalizeHandler
|
|
38
|
+
) async throws -> AsyncThrowingStream<StreamingRecordingEvent, Error>
|
|
39
|
+
func cancelCurrentOperation() async throws
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
struct BotaDeviceSDKSharedAppleRecordingClient: BotaDeviceSDKAppleRecordingClient {
|
|
43
|
+
private let recordings: RecordingManager
|
|
44
|
+
|
|
45
|
+
init(client: BotaDeviceClient = .shared) {
|
|
46
|
+
recordings = client.recordings
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
func listRecordings(_ device: ConnectedDevice) async throws -> [DeviceRecording] {
|
|
50
|
+
try await recordings.listRecordings(device)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
func syncRecording(
|
|
54
|
+
_ device: ConnectedDevice,
|
|
55
|
+
recording: DeviceRecording,
|
|
56
|
+
sinkID: String
|
|
57
|
+
) async throws -> AsyncThrowingStream<RecordingSyncEvent, Error> {
|
|
58
|
+
try await recordings.syncRecording(
|
|
59
|
+
device,
|
|
60
|
+
recording: recording,
|
|
61
|
+
sinkID: sinkID,
|
|
62
|
+
confirmOnCompletion: false
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
func transferMetadata(sinkID: String) async -> RecordingTransferMetadata? {
|
|
67
|
+
await recordings.transferMetadata(sinkID: sinkID)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
func syncEncryptedRecordingV2(
|
|
71
|
+
_ device: ConnectedDevice,
|
|
72
|
+
recording: EncryptedUploadV2Recording,
|
|
73
|
+
provider: @escaping EncryptedUploadV2ProfileProvider
|
|
74
|
+
) async throws {
|
|
75
|
+
try await recordings.syncEncryptedRecordingV2(
|
|
76
|
+
device,
|
|
77
|
+
recording: recording,
|
|
78
|
+
provider: provider
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
func confirmRecording(_ device: ConnectedDevice, recordingUUID: String) async throws {
|
|
83
|
+
try await recordings.confirmRecording(device, recordingUUID: recordingUUID)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
func observeUploadOwnership(
|
|
87
|
+
_ device: ConnectedDevice,
|
|
88
|
+
recordingUUID: String,
|
|
89
|
+
uploadID: String,
|
|
90
|
+
destinationID: String
|
|
91
|
+
) async throws -> AsyncThrowingStream<UploadOwnershipEvent, Error> {
|
|
92
|
+
try await recordings.observeUploadOwnership(
|
|
93
|
+
device,
|
|
94
|
+
recordingUUID: recordingUUID,
|
|
95
|
+
uploadID: uploadID,
|
|
96
|
+
destinationID: destinationID
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
func streamRecording(
|
|
101
|
+
_ device: ConnectedDevice,
|
|
102
|
+
recordingUUID: String,
|
|
103
|
+
sinkID: String,
|
|
104
|
+
chunkSizeBytes: Int,
|
|
105
|
+
flushIntervalMilliseconds: UInt64,
|
|
106
|
+
destinationProvider: @escaping StreamingChunkDestinationProvider,
|
|
107
|
+
finalize: @escaping StreamingFinalizeHandler
|
|
108
|
+
) async throws -> AsyncThrowingStream<StreamingRecordingEvent, Error> {
|
|
109
|
+
try await recordings.streamRecording(
|
|
110
|
+
device,
|
|
111
|
+
recordingUUID: recordingUUID,
|
|
112
|
+
sinkID: sinkID,
|
|
113
|
+
chunkSizeBytes: chunkSizeBytes,
|
|
114
|
+
flushIntervalMilliseconds: flushIntervalMilliseconds,
|
|
115
|
+
destinationProvider: destinationProvider,
|
|
116
|
+
finalize: finalize
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
func cancelCurrentOperation() async throws {
|
|
121
|
+
try await recordings.cancelCurrentOperation()
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
actor BotaDeviceSDKAppleRecordings {
|
|
126
|
+
private enum RecordingError: LocalizedError {
|
|
127
|
+
case missingNativeFile
|
|
128
|
+
case missingUploadOwnershipResult
|
|
129
|
+
case invalidEncryptedUploadV2Selection
|
|
130
|
+
case encryptedUploadV2MaterialRejected
|
|
131
|
+
case missingEncryptedUploadV2Request
|
|
132
|
+
case invalidStreamingDestination
|
|
133
|
+
case requestRejected(String)
|
|
134
|
+
|
|
135
|
+
var errorDescription: String? {
|
|
136
|
+
switch self {
|
|
137
|
+
case .missingNativeFile:
|
|
138
|
+
"recording transfer completed without a native file"
|
|
139
|
+
case .missingUploadOwnershipResult:
|
|
140
|
+
"upload ownership completed without a result"
|
|
141
|
+
case .invalidEncryptedUploadV2Selection:
|
|
142
|
+
"encrypted upload v2 requires an explicit matching profile"
|
|
143
|
+
case .encryptedUploadV2MaterialRejected:
|
|
144
|
+
"encrypted upload v2 material was rejected"
|
|
145
|
+
case .missingEncryptedUploadV2Request:
|
|
146
|
+
"encrypted upload v2 profile request is no longer pending"
|
|
147
|
+
case .invalidStreamingDestination:
|
|
148
|
+
"streaming upload destination is invalid"
|
|
149
|
+
case let .requestRejected(message):
|
|
150
|
+
message
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private let client: any BotaDeviceSDKAppleRecordingClient
|
|
156
|
+
private struct EncryptedUploadV2Request {
|
|
157
|
+
let recording: EncryptedUploadV2Recording
|
|
158
|
+
let continuation: CheckedContinuation<EncryptedUploadV2Material, Error>
|
|
159
|
+
}
|
|
160
|
+
private var encryptedUploadV2Requests: [String: EncryptedUploadV2Request] = [:]
|
|
161
|
+
private var destinationRequests: [
|
|
162
|
+
String: CheckedContinuation<StreamingUploadDestination, Error>
|
|
163
|
+
] = [:]
|
|
164
|
+
private var finalizeRequests: [String: CheckedContinuation<Void, Error>] = [:]
|
|
165
|
+
private var activeStreamingSessionID: String?
|
|
166
|
+
|
|
167
|
+
init(
|
|
168
|
+
client: any BotaDeviceSDKAppleRecordingClient =
|
|
169
|
+
BotaDeviceSDKSharedAppleRecordingClient()
|
|
170
|
+
) {
|
|
171
|
+
self.client = client
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
func listRecordings(_ device: ConnectedDevice) async throws -> [DeviceRecording] {
|
|
175
|
+
try await client.listRecordings(device)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
func syncRecording(
|
|
179
|
+
_ device: ConnectedDevice,
|
|
180
|
+
recording: DeviceRecording,
|
|
181
|
+
sinkID: String,
|
|
182
|
+
onProgress: @escaping @Sendable (RecordingTransferProgress) -> Void
|
|
183
|
+
) async throws -> BotaDeviceSDKAppleRecordingFile {
|
|
184
|
+
let events = try await client.syncRecording(
|
|
185
|
+
device,
|
|
186
|
+
recording: recording,
|
|
187
|
+
sinkID: sinkID
|
|
188
|
+
)
|
|
189
|
+
var path: String?
|
|
190
|
+
for try await event in events {
|
|
191
|
+
switch event {
|
|
192
|
+
case let .progress(progress):
|
|
193
|
+
onProgress(progress)
|
|
194
|
+
case let .completed(url):
|
|
195
|
+
path = url.path
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
guard let path else { throw RecordingError.missingNativeFile }
|
|
199
|
+
let metadata = await client.transferMetadata(sinkID: sinkID)
|
|
200
|
+
return BotaDeviceSDKAppleRecordingFile(
|
|
201
|
+
localPath: path,
|
|
202
|
+
isE2EEncrypted: metadata?.isE2EEncrypted ?? false,
|
|
203
|
+
contentSHA256Hex: metadata?.contentSHA256Hex
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
func syncEncryptedRecordingV2(
|
|
208
|
+
_ device: ConnectedDevice,
|
|
209
|
+
recording: EncryptedUploadV2Recording,
|
|
210
|
+
operationID: String,
|
|
211
|
+
onProfileRequest: @escaping @Sendable ([String: Any]) -> Void,
|
|
212
|
+
onProgress: @escaping @Sendable ([String: Any]) -> Void
|
|
213
|
+
) async throws {
|
|
214
|
+
do {
|
|
215
|
+
try await client.syncEncryptedRecordingV2(
|
|
216
|
+
device,
|
|
217
|
+
recording: recording
|
|
218
|
+
) { context in
|
|
219
|
+
let completedBytes = String(context.checkpoint?.nextCiphertextOffset ?? 0)
|
|
220
|
+
let checkpointRevision = context.checkpoint?.revision
|
|
221
|
+
onProgress(Self.encryptedUploadV2Progress(
|
|
222
|
+
operationID: operationID,
|
|
223
|
+
recordingUUID: recording.uuid,
|
|
224
|
+
phase: "profile_requested",
|
|
225
|
+
completedBytes: completedBytes,
|
|
226
|
+
totalBytes: String(recording.ciphertextLength),
|
|
227
|
+
checkpointRevision: checkpointRevision
|
|
228
|
+
))
|
|
229
|
+
let material = try await self.requestEncryptedUploadV2Profile(
|
|
230
|
+
operationID: operationID,
|
|
231
|
+
context: context,
|
|
232
|
+
onRequest: onProfileRequest
|
|
233
|
+
)
|
|
234
|
+
onProgress(Self.encryptedUploadV2Progress(
|
|
235
|
+
operationID: operationID,
|
|
236
|
+
recordingUUID: recording.uuid,
|
|
237
|
+
phase: "transferring",
|
|
238
|
+
completedBytes: completedBytes,
|
|
239
|
+
totalBytes: String(recording.ciphertextLength),
|
|
240
|
+
checkpointRevision: checkpointRevision
|
|
241
|
+
))
|
|
242
|
+
return material
|
|
243
|
+
}
|
|
244
|
+
onProgress(Self.encryptedUploadV2Progress(
|
|
245
|
+
operationID: operationID,
|
|
246
|
+
recordingUUID: recording.uuid,
|
|
247
|
+
phase: "completed",
|
|
248
|
+
completedBytes: String(recording.ciphertextLength),
|
|
249
|
+
totalBytes: String(recording.ciphertextLength)
|
|
250
|
+
))
|
|
251
|
+
} catch {
|
|
252
|
+
let failure = Self.encryptedUploadV2Failure(error)
|
|
253
|
+
onProgress(Self.encryptedUploadV2Progress(
|
|
254
|
+
operationID: operationID,
|
|
255
|
+
recordingUUID: recording.uuid,
|
|
256
|
+
phase: "failed",
|
|
257
|
+
completedBytes: "0",
|
|
258
|
+
totalBytes: String(recording.ciphertextLength),
|
|
259
|
+
errorCode: failure.code,
|
|
260
|
+
retryable: failure.retryable,
|
|
261
|
+
protocolStatus: failure.protocolStatus
|
|
262
|
+
))
|
|
263
|
+
throw error
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
func resolveEncryptedUploadV2Profile(
|
|
268
|
+
requestID: String,
|
|
269
|
+
profile: String,
|
|
270
|
+
uploadSessionID: String,
|
|
271
|
+
ownerRevision: UInt32,
|
|
272
|
+
securityPolicy: String,
|
|
273
|
+
materialRegistrationID: String
|
|
274
|
+
) throws {
|
|
275
|
+
guard let request = encryptedUploadV2Requests.removeValue(forKey: requestID) else {
|
|
276
|
+
throw RecordingError.missingEncryptedUploadV2Request
|
|
277
|
+
}
|
|
278
|
+
do {
|
|
279
|
+
guard profile == "encrypted_upload_v2",
|
|
280
|
+
let sessionID = UUID(uuidString: uploadSessionID)
|
|
281
|
+
else {
|
|
282
|
+
throw RecordingError.invalidEncryptedUploadV2Selection
|
|
283
|
+
}
|
|
284
|
+
let material = try BotaDeviceSDKEncryptedUploadV2Materials.consume(
|
|
285
|
+
materialRegistrationID,
|
|
286
|
+
recording: request.recording,
|
|
287
|
+
uploadSessionID: sessionID,
|
|
288
|
+
ownerRevision: ownerRevision,
|
|
289
|
+
securityPolicy: securityPolicy
|
|
290
|
+
)
|
|
291
|
+
request.continuation.resume(returning: material)
|
|
292
|
+
} catch {
|
|
293
|
+
request.continuation.resume(throwing: error)
|
|
294
|
+
throw error
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
func rejectEncryptedUploadV2Profile(requestID: String, errorCode: String) throws {
|
|
299
|
+
guard let request = encryptedUploadV2Requests.removeValue(forKey: requestID) else {
|
|
300
|
+
throw RecordingError.missingEncryptedUploadV2Request
|
|
301
|
+
}
|
|
302
|
+
guard errorCode == "application_material_rejected" else {
|
|
303
|
+
request.continuation.resume(throwing: RecordingError.invalidEncryptedUploadV2Selection)
|
|
304
|
+
throw RecordingError.invalidEncryptedUploadV2Selection
|
|
305
|
+
}
|
|
306
|
+
request.continuation.resume(throwing: RecordingError.encryptedUploadV2MaterialRejected)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
func observeUploadOwnership(
|
|
310
|
+
_ device: ConnectedDevice,
|
|
311
|
+
recordingUUID: String,
|
|
312
|
+
uploadID: String,
|
|
313
|
+
destinationID: String,
|
|
314
|
+
onProgress: @escaping @Sendable (RecordingTransferProgress) -> Void
|
|
315
|
+
) async throws -> UploadOwnershipResult {
|
|
316
|
+
let events = try await client.observeUploadOwnership(
|
|
317
|
+
device,
|
|
318
|
+
recordingUUID: recordingUUID,
|
|
319
|
+
uploadID: uploadID,
|
|
320
|
+
destinationID: destinationID
|
|
321
|
+
)
|
|
322
|
+
var result: UploadOwnershipResult?
|
|
323
|
+
for try await event in events {
|
|
324
|
+
switch event {
|
|
325
|
+
case let .progress(progress):
|
|
326
|
+
onProgress(progress)
|
|
327
|
+
case let .result(value):
|
|
328
|
+
result = value
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
guard let result else { throw RecordingError.missingUploadOwnershipResult }
|
|
332
|
+
return result
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
func confirmRecording(_ device: ConnectedDevice, recordingUUID: String) async throws {
|
|
336
|
+
try await client.confirmRecording(device, recordingUUID: recordingUUID)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
func streamRecording(
|
|
340
|
+
_ device: ConnectedDevice,
|
|
341
|
+
recordingUUID: String,
|
|
342
|
+
sessionID: String,
|
|
343
|
+
chunkSizeBytes: Int,
|
|
344
|
+
flushIntervalMilliseconds: UInt64,
|
|
345
|
+
onProgress: @escaping @Sendable ([String: Any]) -> Void,
|
|
346
|
+
onDestinationRequest: @escaping @Sendable ([String: Any]) -> Void,
|
|
347
|
+
onFinalizeRequest: @escaping @Sendable ([String: Any]) -> Void
|
|
348
|
+
) async throws -> UInt64 {
|
|
349
|
+
activeStreamingSessionID = sessionID
|
|
350
|
+
var bytesReceived: UInt64 = 0
|
|
351
|
+
var chunksUploaded: UInt32 = 0
|
|
352
|
+
onProgress(Self.progress(
|
|
353
|
+
sessionID: sessionID,
|
|
354
|
+
state: "streaming",
|
|
355
|
+
bytesReceived: bytesReceived,
|
|
356
|
+
chunksUploaded: chunksUploaded
|
|
357
|
+
))
|
|
358
|
+
defer { activeStreamingSessionID = nil }
|
|
359
|
+
let events = try await client.streamRecording(
|
|
360
|
+
device,
|
|
361
|
+
recordingUUID: recordingUUID,
|
|
362
|
+
sinkID: sessionID,
|
|
363
|
+
chunkSizeBytes: chunkSizeBytes,
|
|
364
|
+
flushIntervalMilliseconds: flushIntervalMilliseconds,
|
|
365
|
+
destinationProvider: { request in
|
|
366
|
+
try await self.requestDestination(
|
|
367
|
+
sessionID: sessionID,
|
|
368
|
+
request: request,
|
|
369
|
+
onRequest: onDestinationRequest
|
|
370
|
+
)
|
|
371
|
+
},
|
|
372
|
+
finalize: { metadata in
|
|
373
|
+
try await self.requestFinalize(
|
|
374
|
+
sessionID: sessionID,
|
|
375
|
+
metadata: metadata,
|
|
376
|
+
onRequest: onFinalizeRequest
|
|
377
|
+
)
|
|
378
|
+
}
|
|
379
|
+
)
|
|
380
|
+
for try await event in events {
|
|
381
|
+
switch event {
|
|
382
|
+
case let .paused(completedBytes):
|
|
383
|
+
bytesReceived = completedBytes
|
|
384
|
+
onProgress(Self.progress(
|
|
385
|
+
sessionID: sessionID,
|
|
386
|
+
state: "paused",
|
|
387
|
+
bytesReceived: bytesReceived,
|
|
388
|
+
chunksUploaded: chunksUploaded
|
|
389
|
+
))
|
|
390
|
+
case .resumed:
|
|
391
|
+
onProgress(Self.progress(
|
|
392
|
+
sessionID: sessionID,
|
|
393
|
+
state: "streaming",
|
|
394
|
+
bytesReceived: bytesReceived,
|
|
395
|
+
chunksUploaded: chunksUploaded
|
|
396
|
+
))
|
|
397
|
+
case let .completed(totalBytes, uploaded, _):
|
|
398
|
+
bytesReceived = totalBytes
|
|
399
|
+
chunksUploaded = uploaded
|
|
400
|
+
onProgress(Self.progress(
|
|
401
|
+
sessionID: sessionID,
|
|
402
|
+
state: "completing",
|
|
403
|
+
bytesReceived: bytesReceived,
|
|
404
|
+
chunksUploaded: chunksUploaded
|
|
405
|
+
))
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return bytesReceived
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
func resolveStreamingDestination(
|
|
412
|
+
requestID: String,
|
|
413
|
+
url: String,
|
|
414
|
+
method: String,
|
|
415
|
+
contentType: String,
|
|
416
|
+
bearerToken: String?
|
|
417
|
+
) {
|
|
418
|
+
guard let continuation = destinationRequests.removeValue(forKey: requestID) else { return }
|
|
419
|
+
guard let url = URL(string: url),
|
|
420
|
+
let method = StreamingUploadMethod(rawValue: method)
|
|
421
|
+
else {
|
|
422
|
+
continuation.resume(throwing: RecordingError.invalidStreamingDestination)
|
|
423
|
+
return
|
|
424
|
+
}
|
|
425
|
+
continuation.resume(returning: .init(
|
|
426
|
+
url: url,
|
|
427
|
+
method: method,
|
|
428
|
+
contentType: contentType,
|
|
429
|
+
bearerToken: bearerToken
|
|
430
|
+
))
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
func rejectStreamingDestination(requestID: String, message: String) {
|
|
434
|
+
destinationRequests.removeValue(forKey: requestID)?.resume(
|
|
435
|
+
throwing: RecordingError.requestRejected(message)
|
|
436
|
+
)
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
func resolveStreamingFinalize(requestID: String) {
|
|
440
|
+
finalizeRequests.removeValue(forKey: requestID)?.resume()
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
func rejectStreamingFinalize(requestID: String, message: String) {
|
|
444
|
+
finalizeRequests.removeValue(forKey: requestID)?.resume(
|
|
445
|
+
throwing: RecordingError.requestRejected(message)
|
|
446
|
+
)
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
func abortStreaming(sessionID: String) async {
|
|
450
|
+
guard activeStreamingSessionID == sessionID else { return }
|
|
451
|
+
rejectPendingRequests(message: "streaming session was aborted")
|
|
452
|
+
try? await client.cancelCurrentOperation()
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
func cancelAll() async {
|
|
456
|
+
rejectPendingRequests(message: "recording operations were cancelled")
|
|
457
|
+
try? await client.cancelCurrentOperation()
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
private func requestEncryptedUploadV2Profile(
|
|
461
|
+
operationID: String,
|
|
462
|
+
context: EncryptedUploadV2ProviderContext,
|
|
463
|
+
onRequest: @escaping @Sendable ([String: Any]) -> Void
|
|
464
|
+
) async throws -> EncryptedUploadV2Material {
|
|
465
|
+
let requestID = UUID().uuidString.lowercased()
|
|
466
|
+
return try await withCheckedThrowingContinuation { continuation in
|
|
467
|
+
encryptedUploadV2Requests[requestID] = .init(
|
|
468
|
+
recording: context.recording,
|
|
469
|
+
continuation: continuation
|
|
470
|
+
)
|
|
471
|
+
var request: [String: Any] = [
|
|
472
|
+
"requestId": requestID,
|
|
473
|
+
"operationId": operationID,
|
|
474
|
+
"recording": Self.encryptedUploadV2Recording(context.recording),
|
|
475
|
+
"capability": Self.encryptedUploadV2Capability(context.capability),
|
|
476
|
+
]
|
|
477
|
+
if let checkpoint = context.checkpoint {
|
|
478
|
+
request["checkpoint"] = Self.encryptedUploadV2Checkpoint(checkpoint)
|
|
479
|
+
}
|
|
480
|
+
onRequest(request)
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
private func requestDestination(
|
|
485
|
+
sessionID: String,
|
|
486
|
+
request: StreamingChunkRequest,
|
|
487
|
+
onRequest: @escaping @Sendable ([String: Any]) -> Void
|
|
488
|
+
) async throws -> StreamingUploadDestination {
|
|
489
|
+
let requestID = UUID().uuidString
|
|
490
|
+
return try await withCheckedThrowingContinuation { continuation in
|
|
491
|
+
destinationRequests[requestID] = continuation
|
|
492
|
+
onRequest([
|
|
493
|
+
"requestId": requestID,
|
|
494
|
+
"sessionId": sessionID,
|
|
495
|
+
"sequence": request.sequence,
|
|
496
|
+
"encrypted": request.isEncrypted,
|
|
497
|
+
])
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
private func requestFinalize(
|
|
502
|
+
sessionID: String,
|
|
503
|
+
metadata: StreamingFinalizeMetadata,
|
|
504
|
+
onRequest: @escaping @Sendable ([String: Any]) -> Void
|
|
505
|
+
) async throws {
|
|
506
|
+
let requestID = UUID().uuidString
|
|
507
|
+
try await withCheckedThrowingContinuation { continuation in
|
|
508
|
+
finalizeRequests[requestID] = continuation
|
|
509
|
+
onRequest([
|
|
510
|
+
"requestId": requestID,
|
|
511
|
+
"sessionId": sessionID,
|
|
512
|
+
"totalChunks": metadata.totalChunks,
|
|
513
|
+
"durationMs": metadata.durationMilliseconds,
|
|
514
|
+
"fileSizeBytes": metadata.fileSizeBytes,
|
|
515
|
+
"encrypted": metadata.isEncrypted,
|
|
516
|
+
])
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
private func rejectPendingRequests(message: String) {
|
|
521
|
+
let error = RecordingError.requestRejected(message)
|
|
522
|
+
let encryptedUploadV2 = encryptedUploadV2Requests.values
|
|
523
|
+
let destinations = destinationRequests.values
|
|
524
|
+
let finalizations = finalizeRequests.values
|
|
525
|
+
destinationRequests.removeAll()
|
|
526
|
+
finalizeRequests.removeAll()
|
|
527
|
+
encryptedUploadV2Requests.removeAll()
|
|
528
|
+
encryptedUploadV2.forEach { $0.continuation.resume(throwing: error) }
|
|
529
|
+
destinations.forEach { $0.resume(throwing: error) }
|
|
530
|
+
finalizations.forEach { $0.resume(throwing: error) }
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
private static func encryptedUploadV2Recording(
|
|
534
|
+
_ recording: EncryptedUploadV2Recording
|
|
535
|
+
) -> [String: Any] {
|
|
536
|
+
[
|
|
537
|
+
"uuid": recording.uuid,
|
|
538
|
+
"generation": recording.generation,
|
|
539
|
+
"ciphertextLength": String(recording.ciphertextLength),
|
|
540
|
+
"ciphertextSha256": recording.ciphertextSHA256.hexString,
|
|
541
|
+
]
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
private static func encryptedUploadV2Capability(
|
|
545
|
+
_ snapshot: EncryptedUploadV2CapabilitySnapshot
|
|
546
|
+
) -> [String: Any] {
|
|
547
|
+
let values = snapshot.capabilities
|
|
548
|
+
return [
|
|
549
|
+
"encodingVersion": Int(snapshot.rawValue.first ?? 0),
|
|
550
|
+
"transferProfileVersion": Int(snapshot.rawValue.dropFirst().first ?? 0),
|
|
551
|
+
"rawValueHex": snapshot.rawValue.hexString,
|
|
552
|
+
"sha256Hex": snapshot.sha256.hexString,
|
|
553
|
+
"flags": values.flags,
|
|
554
|
+
"maximumSignedBlobBytes": values.maximumSignedBlobBytes,
|
|
555
|
+
"maximumManifestBytes": values.maximumManifestBytes,
|
|
556
|
+
"maximumDataPayloadBytes": values.maximumDataPayloadBytes,
|
|
557
|
+
"maximumWindowPackets": values.maximumWindowPackets,
|
|
558
|
+
"durableCheckpointIntervalBlocks": values.durableCheckpointIntervalBlocks,
|
|
559
|
+
"maximumMissingSequences": values.maximumMissingSequences,
|
|
560
|
+
]
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
private static func encryptedUploadV2Checkpoint(
|
|
564
|
+
_ checkpoint: EncryptedUploadV2Checkpoint
|
|
565
|
+
) -> [String: Any] {
|
|
566
|
+
var value: [String: Any] = [
|
|
567
|
+
"version": 1,
|
|
568
|
+
"uploadSessionId": checkpoint.uploadSessionID.uuidString.lowercased(),
|
|
569
|
+
"ownerRevision": checkpoint.ownerRevision,
|
|
570
|
+
"revision": checkpoint.revision,
|
|
571
|
+
"nextCiphertextOffset": String(checkpoint.nextCiphertextOffset),
|
|
572
|
+
"prefixSha256": checkpoint.prefixSHA256.hexString,
|
|
573
|
+
"transportSessionId": String(checkpoint.transportSessionID),
|
|
574
|
+
"sinkRegistrationId": checkpoint.sinkID,
|
|
575
|
+
"windowPackets": checkpoint.windowPackets,
|
|
576
|
+
"dataPayloadBytes": checkpoint.dataPayloadBytes,
|
|
577
|
+
]
|
|
578
|
+
if let sequence = checkpoint.highestContiguousSequence {
|
|
579
|
+
value["highestContiguousSequence"] = sequence
|
|
580
|
+
}
|
|
581
|
+
return value
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
private static func encryptedUploadV2Progress(
|
|
585
|
+
operationID: String,
|
|
586
|
+
recordingUUID: String,
|
|
587
|
+
phase: String,
|
|
588
|
+
completedBytes: String,
|
|
589
|
+
totalBytes: String,
|
|
590
|
+
checkpointRevision: UInt32? = nil,
|
|
591
|
+
errorCode: String? = nil,
|
|
592
|
+
retryable: Bool? = nil,
|
|
593
|
+
protocolStatus: UInt16? = nil
|
|
594
|
+
) -> [String: Any] {
|
|
595
|
+
var value: [String: Any] = [
|
|
596
|
+
"operationId": operationID,
|
|
597
|
+
"recordingUuid": recordingUUID,
|
|
598
|
+
"phase": phase,
|
|
599
|
+
"completedBytes": completedBytes,
|
|
600
|
+
"totalBytes": totalBytes,
|
|
601
|
+
]
|
|
602
|
+
if let checkpointRevision { value["checkpointRevision"] = checkpointRevision }
|
|
603
|
+
if let errorCode { value["errorCode"] = errorCode }
|
|
604
|
+
if let retryable { value["retryable"] = retryable }
|
|
605
|
+
if let protocolStatus { value["protocolStatus"] = protocolStatus }
|
|
606
|
+
return value
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
private static func encryptedUploadV2Failure(
|
|
610
|
+
_ error: Error
|
|
611
|
+
) -> (code: String, retryable: Bool, protocolStatus: UInt16?) {
|
|
612
|
+
guard let sdkError = error as? BotaSDKError else {
|
|
613
|
+
return (
|
|
614
|
+
error is CancellationError ? "cancelled" : "application_material_rejected",
|
|
615
|
+
false,
|
|
616
|
+
nil
|
|
617
|
+
)
|
|
618
|
+
}
|
|
619
|
+
return (sdkError.code.stableName, sdkError.retryable, sdkError.protocolStatus)
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
private static func progress(
|
|
623
|
+
sessionID: String,
|
|
624
|
+
state: String,
|
|
625
|
+
bytesReceived: UInt64,
|
|
626
|
+
chunksUploaded: UInt32
|
|
627
|
+
) -> [String: Any] {
|
|
628
|
+
[
|
|
629
|
+
"sessionId": sessionID,
|
|
630
|
+
"state": state,
|
|
631
|
+
"bytesReceived": bytesReceived,
|
|
632
|
+
"chunksUploaded": chunksUploaded,
|
|
633
|
+
]
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
private extension Data {
|
|
638
|
+
var hexString: String {
|
|
639
|
+
map { String(format: "%02x", $0) }.joined()
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
private extension BotaSDKErrorCode {
|
|
644
|
+
var stableName: String {
|
|
645
|
+
switch self {
|
|
646
|
+
case .invalidInput: "invalid_input"
|
|
647
|
+
case .truncatedPacket: "truncated_packet"
|
|
648
|
+
case .unknownPacket: "unknown_packet"
|
|
649
|
+
case .payloadTooLarge: "payload_too_large"
|
|
650
|
+
case .unsupportedCapability: "unsupported_capability"
|
|
651
|
+
case .unsupportedOperation: "unsupported_operation"
|
|
652
|
+
case .featureUnavailable: "feature_unavailable"
|
|
653
|
+
case .operationInProgress: "operation_in_progress"
|
|
654
|
+
case .unexpectedEvent: "unexpected_event"
|
|
655
|
+
case .deviceNotFound: "device_not_found"
|
|
656
|
+
case .identityMismatch: "identity_mismatch"
|
|
657
|
+
case .connectionFailed: "connection_failed"
|
|
658
|
+
case .persistenceFailed: "persistence_failed"
|
|
659
|
+
case .notConnected: "not_connected"
|
|
660
|
+
case .timeout: "timeout"
|
|
661
|
+
case .cancelled: "cancelled"
|
|
662
|
+
case .protocolRejected: "protocol_rejected"
|
|
663
|
+
case .integrityFailed: "integrity_failed"
|
|
664
|
+
case .uploadOwnershipUnknown: "upload_ownership_unknown"
|
|
665
|
+
case .downloadFailed: "download_failed"
|
|
666
|
+
case .internal: "internal"
|
|
667
|
+
case .unknown: "unknown"
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|