@capgo/capacitor-ricoh360 8.0.12
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/CapgoCapacitorRicoh360.podspec +18 -0
- package/LICENSE +373 -0
- package/Package.swift +30 -0
- package/README.md +314 -0
- package/android/build.gradle +59 -0
- package/android/src/main/AndroidManifest.xml +6 -0
- package/android/src/main/java/ee/forgr/capacitor/ricoh/camera/Ricoh360CameraPlugin.java +453 -0
- package/android/src/main/res/.gitkeep +0 -0
- package/dist/docs.json +496 -0
- package/dist/esm/definitions.d.ts +146 -0
- package/dist/esm/definitions.js +5 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +17 -0
- package/dist/esm/web.js +37 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +51 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +54 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Sources/Ricoh360CameraPlugin/Ricoh360CameraPlugin.swift +580 -0
- package/ios/Tests/Ricoh360CameraPluginTests/Ricoh360CameraPluginTests.swift +15 -0
- package/package.json +94 -0
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Capacitor
|
|
3
|
+
import Alamofire
|
|
4
|
+
import AVKit
|
|
5
|
+
import UIKit
|
|
6
|
+
import Network
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Please read the Capacitor iOS Plugin Development Guide
|
|
10
|
+
* here: https://capacitorjs.com/docs/plugins/ios
|
|
11
|
+
*/
|
|
12
|
+
@objc(Ricoh360CameraPlugin)
|
|
13
|
+
public class Ricoh360CameraPlugin: CAPPlugin, CAPBridgedPlugin, URLSessionDataDelegate { // swiftlint:disable:this type_body_length
|
|
14
|
+
private let pluginVersion: String = "8.0.12"
|
|
15
|
+
public let identifier = "Ricoh360CameraPlugin"
|
|
16
|
+
public let jsName = "Ricoh360Camera"
|
|
17
|
+
public let pluginMethods: [CAPPluginMethod] = [
|
|
18
|
+
CAPPluginMethod(name: "initialize", returnType: CAPPluginReturnPromise),
|
|
19
|
+
CAPPluginMethod(name: "capturePicture", returnType: CAPPluginReturnPromise),
|
|
20
|
+
CAPPluginMethod(name: "readSettings", returnType: CAPPluginReturnPromise),
|
|
21
|
+
CAPPluginMethod(name: "setSettings", returnType: CAPPluginReturnPromise),
|
|
22
|
+
CAPPluginMethod(name: "livePreview", returnType: CAPPluginReturnPromise),
|
|
23
|
+
CAPPluginMethod(name: "stopLivePreview", returnType: CAPPluginReturnPromise),
|
|
24
|
+
CAPPluginMethod(name: "captureVideo", returnType: CAPPluginReturnPromise),
|
|
25
|
+
CAPPluginMethod(name: "sendCommand", returnType: CAPPluginReturnPromise),
|
|
26
|
+
CAPPluginMethod(name: "getCameraAsset", returnType: CAPPluginReturnPromise),
|
|
27
|
+
CAPPluginMethod(name: "listFiles", returnType: CAPPluginReturnPromise),
|
|
28
|
+
CAPPluginMethod(name: "getPluginVersion", returnType: CAPPluginReturnPromise)
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
private var previewView: UIImageView?
|
|
32
|
+
private var cameraUrl: String = "http://192.168.1.1"
|
|
33
|
+
private var httpStream: HttpStream?
|
|
34
|
+
private var connection: NWConnection?
|
|
35
|
+
private var browser: NWBrowser?
|
|
36
|
+
private var permissionCompletion: ((Bool) -> Void)?
|
|
37
|
+
|
|
38
|
+
enum RicohThetaError: Error {
|
|
39
|
+
case invalidUrl, badResponse, parseImageProvider, parseCGImage
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private let startMarker = Data([0xFF, 0xD8])
|
|
43
|
+
private let endMarker = Data([0xFF, 0xD9])
|
|
44
|
+
private var buffer = Data()
|
|
45
|
+
private var session: URLSession
|
|
46
|
+
private var task: URLSessionTask?
|
|
47
|
+
private var imageCompletion: ((UIImage?, Error?) -> Void)?
|
|
48
|
+
|
|
49
|
+
override init() {
|
|
50
|
+
let config = URLSessionConfiguration.default
|
|
51
|
+
config.timeoutIntervalForRequest = 5
|
|
52
|
+
config.waitsForConnectivity = true
|
|
53
|
+
self.session = URLSession(configuration: config, delegate: nil, delegateQueue: OperationQueue.main)
|
|
54
|
+
super.init()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
struct CameraInfo: Decodable {
|
|
58
|
+
let model: String
|
|
59
|
+
let firmwareVersion: String
|
|
60
|
+
let manufacturer: String
|
|
61
|
+
let serialNumber: String
|
|
62
|
+
let supportUrl: String
|
|
63
|
+
let gps: String
|
|
64
|
+
let gyro: String
|
|
65
|
+
let uptime: String
|
|
66
|
+
let api: String
|
|
67
|
+
let apiLevel: String
|
|
68
|
+
let endpoints: Endpoints
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
struct Endpoints: Decodable {
|
|
72
|
+
let httpPort: String
|
|
73
|
+
let httpUpdatesPort: String
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
struct CommandResponse: Decodable {
|
|
77
|
+
let name: String
|
|
78
|
+
let state: String
|
|
79
|
+
let results: CommandResults?
|
|
80
|
+
let error: CommandError?
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
struct CommandResults: Decodable {
|
|
84
|
+
// Define fields based on the specific command response
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
struct CommandError: Decodable {
|
|
88
|
+
let code: String
|
|
89
|
+
let message: String
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
protocol HttpStreamProtocol: URLSessionDataDelegate {
|
|
93
|
+
init(request: URLRequest, plugin: Ricoh360CameraPlugin)
|
|
94
|
+
func startLivePreview(completion: @escaping (UIImage?, Error?) -> Void)
|
|
95
|
+
func stopLivePreview()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
final class HttpStream: NSObject, HttpStreamProtocol {
|
|
99
|
+
enum HttpStreamError: Error {
|
|
100
|
+
case badResponse, parseImageProvider, parseCGImage
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let startMarker = Data([0xFF, 0xD8])
|
|
104
|
+
let endMarker = Data([0xFF, 0xD9])
|
|
105
|
+
let queue = DispatchQueue(label: "com.ricoh.theta.stream.queue")
|
|
106
|
+
|
|
107
|
+
var buffer = Data()
|
|
108
|
+
var request: URLRequest
|
|
109
|
+
var task: URLSessionTask?
|
|
110
|
+
var isLivePreviewing: Bool = false
|
|
111
|
+
var imageCompletion: ((UIImage?, Error?) -> Void)?
|
|
112
|
+
weak var plugin: Ricoh360CameraPlugin?
|
|
113
|
+
|
|
114
|
+
deinit {
|
|
115
|
+
self.stopLivePreview()
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
required init(request: URLRequest, plugin: Ricoh360CameraPlugin) {
|
|
119
|
+
self.request = request
|
|
120
|
+
self.plugin = plugin
|
|
121
|
+
super.init()
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
func startLivePreview(completion: @escaping (UIImage?, Error?) -> Void) {
|
|
125
|
+
if !self.isLivePreviewing {
|
|
126
|
+
self.imageCompletion = completion
|
|
127
|
+
let payload = ["name": "camera.getLivePreview"]
|
|
128
|
+
self.request.httpBody = try? JSONEncoder().encode(payload)
|
|
129
|
+
self.request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
130
|
+
self.request.setValue("application/json", forHTTPHeaderField: "Accept")
|
|
131
|
+
|
|
132
|
+
let config = URLSessionConfiguration.default
|
|
133
|
+
config.timeoutIntervalForRequest = 5
|
|
134
|
+
config.waitsForConnectivity = true
|
|
135
|
+
|
|
136
|
+
plugin?.requestLocalNetworkPermission { [weak self] granted in
|
|
137
|
+
guard let self = self else { return }
|
|
138
|
+
|
|
139
|
+
if granted {
|
|
140
|
+
let session = URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue.main)
|
|
141
|
+
self.task = session.dataTask(with: self.request)
|
|
142
|
+
self.task?.resume()
|
|
143
|
+
self.isLivePreviewing = true
|
|
144
|
+
} else {
|
|
145
|
+
self.imageCompletion?(nil, HttpStreamError.badResponse)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
func stopLivePreview() {
|
|
152
|
+
self.task?.cancel()
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
|
|
156
|
+
guard let response = dataTask.response as? HTTPURLResponse, response.statusCode == 200 else {
|
|
157
|
+
self.imageCompletion?(nil, HttpStreamError.badResponse)
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if data.range(of: self.startMarker) != nil {
|
|
162
|
+
self.buffer = Data()
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
self.buffer.append(data)
|
|
166
|
+
|
|
167
|
+
if data.range(of: self.endMarker) != nil {
|
|
168
|
+
self.parseFrame(self.buffer) { [weak self] image, error in
|
|
169
|
+
guard let self = self else { return }
|
|
170
|
+
self.imageCompletion?(image, error)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
|
|
176
|
+
session.invalidateAndCancel()
|
|
177
|
+
self.isLivePreviewing = false
|
|
178
|
+
self.imageCompletion?(nil, error)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private func parseFrame(_ data: Data, _ completion: @escaping ((UIImage?, Error?) -> Void)) {
|
|
182
|
+
self.queue.async {
|
|
183
|
+
guard let imgProvider = CGDataProvider(data: data as CFData) else {
|
|
184
|
+
completion(nil, HttpStreamError.parseImageProvider)
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
guard let image = CGImage(
|
|
189
|
+
jpegDataProviderSource: imgProvider,
|
|
190
|
+
decode: nil,
|
|
191
|
+
shouldInterpolate: true,
|
|
192
|
+
intent: .defaultIntent) else {
|
|
193
|
+
completion(nil, HttpStreamError.parseCGImage)
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
completion(UIImage(cgImage: image), nil)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
@objc func initialize(_ call: CAPPluginCall) {
|
|
203
|
+
if let url = call.getString("url") {
|
|
204
|
+
cameraUrl = url
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
let infoUrl = "\(cameraUrl)/osc/info"
|
|
208
|
+
|
|
209
|
+
requestLocalNetworkPermission { [weak self] granted in
|
|
210
|
+
if granted {
|
|
211
|
+
AF.request(infoUrl).validate().responseData { response in
|
|
212
|
+
switch response.result {
|
|
213
|
+
case .success(let data):
|
|
214
|
+
do {
|
|
215
|
+
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
|
|
216
|
+
call.resolve(["session": "Initialized", "info": json])
|
|
217
|
+
} else {
|
|
218
|
+
call.reject("Camera is not reachable or info could not be retrieved")
|
|
219
|
+
}
|
|
220
|
+
} catch {
|
|
221
|
+
call.reject("Camera is not reachable or info could not be retrieved")
|
|
222
|
+
}
|
|
223
|
+
case .failure(let error):
|
|
224
|
+
call.reject("Camera is not reachable or info could not be retrieved")
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
} else {
|
|
228
|
+
call.reject("Permission not granted")
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
@objc func livePreview(_ call: CAPPluginCall) {
|
|
234
|
+
DispatchQueue.main.async {
|
|
235
|
+
if self.previewView == nil {
|
|
236
|
+
let displayInFront = call.getBool("displayInFront") ?? true
|
|
237
|
+
let cropPreview = call.getBool("cropPreview") ?? false
|
|
238
|
+
|
|
239
|
+
// Create preview view with zero frame initially
|
|
240
|
+
self.previewView = UIImageView(frame: .zero)
|
|
241
|
+
self.previewView?.backgroundColor = .clear
|
|
242
|
+
self.previewView?.contentMode = cropPreview ? .scaleAspectFill : .scaleAspectFit
|
|
243
|
+
self.previewView?.clipsToBounds = cropPreview // Only clip bounds if cropping
|
|
244
|
+
|
|
245
|
+
// Make webview transparent
|
|
246
|
+
self.webView?.isOpaque = false
|
|
247
|
+
self.webView?.backgroundColor = UIColor.clear
|
|
248
|
+
self.webView?.scrollView.backgroundColor = UIColor.clear
|
|
249
|
+
|
|
250
|
+
if let previewView = self.previewView {
|
|
251
|
+
if displayInFront {
|
|
252
|
+
self.bridge?.viewController?.view.addSubview(previewView)
|
|
253
|
+
} else {
|
|
254
|
+
self.webView?.superview?.addSubview(previewView)
|
|
255
|
+
self.webView?.superview?.bringSubviewToFront(self.webView!)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Make preview view fill parent
|
|
259
|
+
previewView.translatesAutoresizingMaskIntoConstraints = false
|
|
260
|
+
NSLayoutConstraint.activate([
|
|
261
|
+
previewView.topAnchor.constraint(equalTo: previewView.superview!.topAnchor),
|
|
262
|
+
previewView.bottomAnchor.constraint(equalTo: previewView.superview!.bottomAnchor),
|
|
263
|
+
previewView.leadingAnchor.constraint(equalTo: previewView.superview!.leadingAnchor),
|
|
264
|
+
previewView.trailingAnchor.constraint(equalTo: previewView.superview!.trailingAnchor)
|
|
265
|
+
])
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
let url = URL(string: "\(self.cameraUrl)/osc/commands/execute")!
|
|
270
|
+
var request = URLRequest(url: url)
|
|
271
|
+
request.httpMethod = "POST"
|
|
272
|
+
|
|
273
|
+
self.httpStream = HttpStream(request: request, plugin: self)
|
|
274
|
+
self.httpStream?.startLivePreview { [weak self] (image: UIImage?, _: Error?) in
|
|
275
|
+
guard let self = self else { return }
|
|
276
|
+
if let image = image {
|
|
277
|
+
DispatchQueue.main.async {
|
|
278
|
+
self.previewView?.image = image
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
call.resolve(["preview": "Started"])
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
@objc func stopLivePreview(_ call: CAPPluginCall) {
|
|
287
|
+
DispatchQueue.main.async {
|
|
288
|
+
self.httpStream?.stopLivePreview()
|
|
289
|
+
self.previewView?.removeFromSuperview()
|
|
290
|
+
self.previewView = nil
|
|
291
|
+
|
|
292
|
+
// Revert webview transparency
|
|
293
|
+
self.webView?.isOpaque = true
|
|
294
|
+
self.webView?.backgroundColor = .white
|
|
295
|
+
self.webView?.scrollView.backgroundColor = .white
|
|
296
|
+
|
|
297
|
+
call.resolve(["preview": "Stopped"])
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
@objc func capturePicture(_ call: CAPPluginCall) {
|
|
302
|
+
let parameters: [String: Any] = ["name": "camera.takePicture"]
|
|
303
|
+
guard let jsonData = try? JSONSerialization.data(withJSONObject: parameters),
|
|
304
|
+
let jsonInputString = String(data: jsonData, encoding: .utf8) else {
|
|
305
|
+
call.reject("Failed to serialize parameters")
|
|
306
|
+
return
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
sendCommandRaw(endpoint: "/osc/commands/execute", payload: jsonInputString, call: call)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
@objc func readSettings(_ call: CAPPluginCall) {
|
|
313
|
+
guard let data = call.options,
|
|
314
|
+
let jsonData = try? JSONSerialization.data(withJSONObject: data),
|
|
315
|
+
let options = String(data: jsonData, encoding: .utf8) else {
|
|
316
|
+
call.reject("Invalid options")
|
|
317
|
+
return
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
let jsonInputString = """
|
|
321
|
+
{"name": "camera.getOptions", "parameters": {"optionNames": \(options)}}
|
|
322
|
+
"""
|
|
323
|
+
|
|
324
|
+
sendCommandRaw(endpoint: "/osc/commands/execute", payload: jsonInputString, call: call)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
@objc func setSettings(_ call: CAPPluginCall) {
|
|
328
|
+
guard let data = call.options,
|
|
329
|
+
let jsonData = try? JSONSerialization.data(withJSONObject: data),
|
|
330
|
+
let options = String(data: jsonData, encoding: .utf8) else {
|
|
331
|
+
call.reject("Invalid options")
|
|
332
|
+
return
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
let jsonInputString = """
|
|
336
|
+
{"name": "camera.setOptions", "parameters": {"options": \(options)}}
|
|
337
|
+
"""
|
|
338
|
+
|
|
339
|
+
sendCommandRaw(endpoint: "/osc/commands/execute", payload: jsonInputString, call: call)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
@objc func sendCommand(_ call: CAPPluginCall) {
|
|
343
|
+
guard let endpoint = call.getString("endpoint"),
|
|
344
|
+
let data = call.getObject("payload"),
|
|
345
|
+
let jsonData = try? JSONSerialization.data(withJSONObject: data),
|
|
346
|
+
let payload = String(data: jsonData, encoding: .utf8) else {
|
|
347
|
+
call.reject("Invalid endpoint or payload")
|
|
348
|
+
return
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
sendCommandRaw(endpoint: endpoint, payload: payload, call: call)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
private func sendCommandRaw(endpoint: String, payload: String, call: CAPPluginCall) {
|
|
355
|
+
guard let url = URL(string: cameraUrl + endpoint) else {
|
|
356
|
+
call.reject("Invalid URL")
|
|
357
|
+
return
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
requestLocalNetworkPermission { [weak self] granted in
|
|
361
|
+
print("Permission flow completed with result: \(granted)")
|
|
362
|
+
if granted {
|
|
363
|
+
print("Proceeding with socket connection")
|
|
364
|
+
var request = URLRequest(url: url)
|
|
365
|
+
request.httpMethod = "POST"
|
|
366
|
+
request.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")
|
|
367
|
+
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
|
368
|
+
request.httpBody = payload.data(using: .utf8)
|
|
369
|
+
|
|
370
|
+
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
|
|
371
|
+
if let error = error {
|
|
372
|
+
call.reject("Command failed", error.localizedDescription)
|
|
373
|
+
return
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
guard let httpResponse = response as? HTTPURLResponse,
|
|
377
|
+
let data = data else {
|
|
378
|
+
call.reject("Command failed")
|
|
379
|
+
return
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if httpResponse.statusCode == 200 {
|
|
383
|
+
if let result = String(data: data, encoding: .utf8),
|
|
384
|
+
let jsonData = result.data(using: .utf8),
|
|
385
|
+
let json = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any] {
|
|
386
|
+
call.resolve(json)
|
|
387
|
+
} else {
|
|
388
|
+
call.reject("Failed to parse command response")
|
|
389
|
+
}
|
|
390
|
+
} else {
|
|
391
|
+
if let error = String(data: data, encoding: .utf8) {
|
|
392
|
+
call.reject("Command failed: \(error)")
|
|
393
|
+
} else {
|
|
394
|
+
call.reject("Command failed")
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
task.resume()
|
|
399
|
+
} else {
|
|
400
|
+
print("Permission not granted")
|
|
401
|
+
call.reject("Permission not granted")
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
private func requestLocalNetworkPermission(completion: @escaping (Bool) -> Void) {
|
|
407
|
+
self.permissionCompletion = completion
|
|
408
|
+
|
|
409
|
+
// Use Bonjour browsing to trigger local network permission
|
|
410
|
+
let parameters = NWParameters()
|
|
411
|
+
parameters.includePeerToPeer = true
|
|
412
|
+
|
|
413
|
+
// Browse for all services
|
|
414
|
+
let browser = NWBrowser(for: .bonjour(type: "_http._tcp", domain: nil), using: parameters)
|
|
415
|
+
self.browser = browser
|
|
416
|
+
|
|
417
|
+
browser.stateUpdateHandler = { [weak self] state in
|
|
418
|
+
switch state {
|
|
419
|
+
case .ready:
|
|
420
|
+
print("Browser is ready")
|
|
421
|
+
self?.permissionCompletion?(true)
|
|
422
|
+
self?.browser?.cancel()
|
|
423
|
+
case .failed(let error):
|
|
424
|
+
print("Browser failed: \(error)")
|
|
425
|
+
if let error = error as? NWError {
|
|
426
|
+
switch error {
|
|
427
|
+
case .dns(let dnsError):
|
|
428
|
+
print("DNS error: \(dnsError)")
|
|
429
|
+
case .posix(let code):
|
|
430
|
+
print("POSIX error: \(code)")
|
|
431
|
+
default:
|
|
432
|
+
print("Other error: \(error)")
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
self?.permissionCompletion?(false)
|
|
436
|
+
self?.browser?.cancel()
|
|
437
|
+
case .cancelled:
|
|
438
|
+
print("Browser was cancelled")
|
|
439
|
+
case .waiting(let error):
|
|
440
|
+
print("Browser is waiting: \(error)")
|
|
441
|
+
default:
|
|
442
|
+
break
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
browser.browseResultsChangedHandler = { results, _ in
|
|
447
|
+
print("Found \(results.count) services")
|
|
448
|
+
for result in results {
|
|
449
|
+
print("Service: \(result.endpoint)")
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
print("Starting network service browser")
|
|
454
|
+
browser.start(queue: .main)
|
|
455
|
+
|
|
456
|
+
// Set a timeout
|
|
457
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { [weak self] in
|
|
458
|
+
if self?.browser != nil {
|
|
459
|
+
print("Browser timeout - proceeding anyway")
|
|
460
|
+
self?.permissionCompletion?(true)
|
|
461
|
+
self?.browser?.cancel()
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
@objc func getCameraAsset(_ call: CAPPluginCall) {
|
|
467
|
+
guard let assetUrl = call.getString("url") else {
|
|
468
|
+
call.reject("URL is required")
|
|
469
|
+
return
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
let saveToFile = call.getBool("saveToFile", false)
|
|
473
|
+
|
|
474
|
+
guard let url = URL(string: assetUrl) else {
|
|
475
|
+
call.reject("Invalid URL")
|
|
476
|
+
return
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
requestLocalNetworkPermission { [weak self] granted in
|
|
480
|
+
guard let self = self else { return }
|
|
481
|
+
|
|
482
|
+
if granted {
|
|
483
|
+
let task = URLSession.shared.dataTask(with: url) { [weak self] (data, response, error) in
|
|
484
|
+
guard let self = self else { return }
|
|
485
|
+
|
|
486
|
+
if let error = error {
|
|
487
|
+
call.reject("Failed to fetch asset: \(error.localizedDescription)")
|
|
488
|
+
return
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
guard let httpResponse = response as? HTTPURLResponse,
|
|
492
|
+
let data = data else {
|
|
493
|
+
call.reject("Failed to fetch asset")
|
|
494
|
+
return
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
var result = JSObject()
|
|
498
|
+
result["statusCode"] = httpResponse.statusCode
|
|
499
|
+
|
|
500
|
+
if saveToFile {
|
|
501
|
+
let timestamp = Int(Date().timeIntervalSince1970 * 1000)
|
|
502
|
+
let filename = "ricoh_\(timestamp).jpg"
|
|
503
|
+
let tempDir = FileManager.default.temporaryDirectory
|
|
504
|
+
let fileURL = tempDir.appendingPathComponent(filename)
|
|
505
|
+
|
|
506
|
+
do {
|
|
507
|
+
try data.write(to: fileURL)
|
|
508
|
+
result["filePath"] = fileURL.path
|
|
509
|
+
call.resolve(result)
|
|
510
|
+
} catch {
|
|
511
|
+
call.reject("Failed to save file: \(error.localizedDescription)")
|
|
512
|
+
}
|
|
513
|
+
} else {
|
|
514
|
+
// Downsize and convert to base64
|
|
515
|
+
if let downsizedData = self.downSizeImage(data) {
|
|
516
|
+
let base64String = downsizedData.base64EncodedString()
|
|
517
|
+
result["data"] = base64String
|
|
518
|
+
call.resolve(result)
|
|
519
|
+
} else {
|
|
520
|
+
call.reject("Failed to process image")
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
task.resume()
|
|
525
|
+
} else {
|
|
526
|
+
call.reject("Permission not granted")
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
private func downSizeImage(_ imageData: Data) -> Data? {
|
|
532
|
+
let maxWidth: CGFloat = 2048 // Must be equal to or less than 4096
|
|
533
|
+
|
|
534
|
+
guard let image = UIImage(data: imageData) else { return nil }
|
|
535
|
+
let originalWidth = image.size.width
|
|
536
|
+
|
|
537
|
+
if originalWidth > maxWidth {
|
|
538
|
+
let scaleFactor = maxWidth / originalWidth
|
|
539
|
+
let newSize = CGSize(width: originalWidth * scaleFactor, height: image.size.height * scaleFactor)
|
|
540
|
+
|
|
541
|
+
UIGraphicsBeginImageContextWithOptions(newSize, false, 1.0)
|
|
542
|
+
image.draw(in: CGRect(origin: .zero, size: newSize))
|
|
543
|
+
guard let resizedImage = UIGraphicsGetImageFromCurrentImageContext() else {
|
|
544
|
+
UIGraphicsEndImageContext()
|
|
545
|
+
return nil
|
|
546
|
+
}
|
|
547
|
+
UIGraphicsEndImageContext()
|
|
548
|
+
|
|
549
|
+
return resizedImage.jpegData(compressionQuality: 1.0)
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
return imageData
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
@objc func listFiles(_ call: CAPPluginCall) {
|
|
556
|
+
var parameters = JSObject()
|
|
557
|
+
parameters["fileType"] = call.getString("fileType", "all")
|
|
558
|
+
parameters["startPosition"] = call.getInt("startPosition", 0)
|
|
559
|
+
parameters["entryCount"] = call.getInt("entryCount", 100)
|
|
560
|
+
parameters["maxThumbSize"] = call.getInt("maxThumbSize", 0)
|
|
561
|
+
parameters["_detail"] = call.getBool("_detail", true)
|
|
562
|
+
|
|
563
|
+
var payload = JSObject()
|
|
564
|
+
payload["name"] = "camera.listFiles"
|
|
565
|
+
payload["parameters"] = parameters
|
|
566
|
+
|
|
567
|
+
guard let jsonData = try? JSONSerialization.data(withJSONObject: payload),
|
|
568
|
+
let jsonInputString = String(data: jsonData, encoding: .utf8) else {
|
|
569
|
+
call.reject("Failed to serialize parameters")
|
|
570
|
+
return
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
sendCommandRaw(endpoint: "/osc/commands/execute", payload: jsonInputString, call: call)
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
@objc func getPluginVersion(_ call: CAPPluginCall) {
|
|
577
|
+
call.resolve(["version": self.pluginVersion])
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
@testable import Ricoh360CameraPlugin
|
|
3
|
+
|
|
4
|
+
class Ricoh360CameraTests: XCTestCase {
|
|
5
|
+
func testEcho() {
|
|
6
|
+
// This is an example of a functional test case for a plugin.
|
|
7
|
+
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
|
8
|
+
|
|
9
|
+
let implementation = Ricoh360Camera()
|
|
10
|
+
let value = "Hello, World!"
|
|
11
|
+
let result = implementation.echo(value)
|
|
12
|
+
|
|
13
|
+
XCTAssertEqual(value, result)
|
|
14
|
+
}
|
|
15
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@capgo/capacitor-ricoh360",
|
|
3
|
+
"version": "8.0.12",
|
|
4
|
+
"description": "Provides an SDK for the Ricoh360 cameras for Capacitor",
|
|
5
|
+
"main": "dist/plugin.cjs.js",
|
|
6
|
+
"module": "dist/esm/index.js",
|
|
7
|
+
"types": "dist/esm/index.d.ts",
|
|
8
|
+
"unpkg": "dist/plugin.js",
|
|
9
|
+
"files": [
|
|
10
|
+
"android/src/main/",
|
|
11
|
+
"android/build.gradle",
|
|
12
|
+
"dist/",
|
|
13
|
+
"ios/Sources",
|
|
14
|
+
"ios/Tests",
|
|
15
|
+
"Package.swift",
|
|
16
|
+
"CapgoCapacitorRicoh360.podspec"
|
|
17
|
+
],
|
|
18
|
+
"author": "",
|
|
19
|
+
"license": "MPL-2.0",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/Cap-go/capacitor-ricoh360-camera-plugin.git"
|
|
23
|
+
},
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/Cap-go/capacitor-ricoh360-camera-plugin/issues"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://capgo.app/docs/plugins/ricoh360-camera/",
|
|
28
|
+
"keywords": [
|
|
29
|
+
"capacitor",
|
|
30
|
+
"plugin",
|
|
31
|
+
"native",
|
|
32
|
+
"ricoh360",
|
|
33
|
+
"camera",
|
|
34
|
+
"360",
|
|
35
|
+
"camera360",
|
|
36
|
+
"360camera",
|
|
37
|
+
"360camera360",
|
|
38
|
+
"360camera360camera",
|
|
39
|
+
"ricoh",
|
|
40
|
+
"capgo"
|
|
41
|
+
],
|
|
42
|
+
"scripts": {
|
|
43
|
+
"verify": "bun run verify:ios && bun run verify:android && bun run verify:web",
|
|
44
|
+
"verify:ios": "xcodebuild -scheme CapgoCapacitorRicoh360 -destination generic/platform=iOS",
|
|
45
|
+
"verify:android": "cd android && ./gradlew clean build test && cd ..",
|
|
46
|
+
"verify:web": "bun run build",
|
|
47
|
+
"lint": "bun run eslint && bun run prettier -- --check && bun run swiftlint -- lint",
|
|
48
|
+
"fmt": "bun run eslint -- --fix && bun run prettier -- --write && bun run swiftlint -- --fix --format",
|
|
49
|
+
"eslint": "eslint . --ext ts",
|
|
50
|
+
"prettier": "prettier-pretty-check \"**/*.{css,html,ts,js,java}\" --plugin=prettier-plugin-java",
|
|
51
|
+
"swiftlint": "node-swiftlint",
|
|
52
|
+
"docgen": "docgen --api Ricoh360CameraPlugin --output-readme README.md --output-json dist/docs.json",
|
|
53
|
+
"build": "bun run clean && bun run docgen && tsc && rollup -c rollup.config.mjs",
|
|
54
|
+
"clean": "rimraf ./dist",
|
|
55
|
+
"watch": "tsc --watch",
|
|
56
|
+
"prepublishOnly": "bun run build",
|
|
57
|
+
"check:wiring": "node scripts/check-capacitor-plugin-wiring.mjs"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@capacitor/android": "^8.0.0",
|
|
61
|
+
"@capacitor/core": "^8.0.0",
|
|
62
|
+
"@capacitor/docgen": "^0.3.1",
|
|
63
|
+
"@capacitor/ios": "^8.0.0",
|
|
64
|
+
"@ionic/eslint-config": "^0.4.0",
|
|
65
|
+
"@ionic/prettier-config": "^4.0.0",
|
|
66
|
+
"@ionic/swiftlint-config": "^2.0.0",
|
|
67
|
+
"@types/node": "^24.10.1",
|
|
68
|
+
"eslint": "^8.57.1",
|
|
69
|
+
"eslint-plugin-import": "^2.31.0",
|
|
70
|
+
"prettier": "^3.6.2",
|
|
71
|
+
"prettier-plugin-java": "^2.7.7",
|
|
72
|
+
"rimraf": "^6.1.0",
|
|
73
|
+
"rollup": "^4.53.2",
|
|
74
|
+
"swiftlint": "^2.0.0",
|
|
75
|
+
"typescript": "^5.9.3",
|
|
76
|
+
"prettier-pretty-check": "^0.2.0"
|
|
77
|
+
},
|
|
78
|
+
"peerDependencies": {
|
|
79
|
+
"@capacitor/core": ">=8.0.0"
|
|
80
|
+
},
|
|
81
|
+
"prettier": "@ionic/prettier-config",
|
|
82
|
+
"swiftlint": "@ionic/swiftlint-config",
|
|
83
|
+
"eslintConfig": {
|
|
84
|
+
"extends": "@ionic/eslint-config/recommended"
|
|
85
|
+
},
|
|
86
|
+
"capacitor": {
|
|
87
|
+
"ios": {
|
|
88
|
+
"src": "ios"
|
|
89
|
+
},
|
|
90
|
+
"android": {
|
|
91
|
+
"src": "android"
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|