diff --git a/.github/workflows/test-kmp.yaml b/.github/workflows/test-kmp.yaml index 210530d597..fe48cce7a5 100644 --- a/.github/workflows/test-kmp.yaml +++ b/.github/workflows/test-kmp.yaml @@ -23,7 +23,7 @@ jobs: arguments: testReleaseUnitTest --info - name: Archive code coverage results if: always() - uses: actions/upload-artifact@v1 + uses: actions/upload-artifact@v4 with: name: test-kmp-report path: kotlin-multiplatform/build/reports/tests/testReleaseUnitTest diff --git a/.github/workflows/test-rn.yaml b/.github/workflows/test-rn.yaml index a493999df1..a6eb9516eb 100644 --- a/.github/workflows/test-rn.yaml +++ b/.github/workflows/test-rn.yaml @@ -24,7 +24,7 @@ jobs: yarn test --collectCoverage - name: Archive code coverage results if: always() - uses: actions/upload-artifact@v1 + uses: actions/upload-artifact@v4 with: name: test-react-native-report path: react-native/coverage diff --git a/demos/demo-android/app/build.gradle b/demos/demo-android/app/build.gradle index 12201de7d0..40624951d8 100755 --- a/demos/demo-android/app/build.gradle +++ b/demos/demo-android/app/build.gradle @@ -75,7 +75,7 @@ dependencies { implementation 'com.jakewharton.timber:timber:5.0.1' implementation 'io.coil-kt:coil-compose:2.2.2' implementation "io.ktor:ktor-client-cio:2.3.9" - implementation "com.ricoh360.thetaclient:theta-client:1.10.2" + implementation "com.ricoh360.thetaclient:theta-client:1.11.0" testImplementation 'org.junit.jupiter:junit-jupiter:5.9.0' testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version" diff --git a/demos/demo-ios/Podfile b/demos/demo-ios/Podfile index 10c4bccc97..48e8f83579 100644 --- a/demos/demo-ios/Podfile +++ b/demos/demo-ios/Podfile @@ -7,5 +7,5 @@ target 'SdkSample' do use_frameworks! # Pods for SdkSample - pod 'THETAClient', '1.10.2' + pod 'THETAClient', '1.11.0' end diff --git a/demos/demo-react-native/package.json b/demos/demo-react-native/package.json index 183f11ed9f..75afe9ebea 100644 --- a/demos/demo-react-native/package.json +++ b/demos/demo-react-native/package.json @@ -13,7 +13,7 @@ "dependencies": { "@react-navigation/native": "^6.1.0", "@react-navigation/native-stack": "^6.9.5", - "theta-client-react-native": "1.10.2", + "theta-client-react-native": "1.11.0", "react": "18.2.0", "react-native": "0.71.14", "react-native-safe-area-context": "^4.4.1", diff --git a/docs/tutorial-android.ja.md b/docs/tutorial-android.ja.md index 7d13ef42bd..3c75b0ce9b 100644 --- a/docs/tutorial-android.ja.md +++ b/docs/tutorial-android.ja.md @@ -4,7 +4,7 @@ - モジュールの`build.gradle`の`dependencies`に次を追加します。 ``` - implementation "com.ricoh360.thetaclient:theta-client:1.10.2" + implementation "com.ricoh360.thetaclient:theta-client:1.11.0" ``` - 本 SDK を使用したアプリケーションが動作するスマートフォンと THETA を無線 LAN 接続しておきます。 diff --git a/docs/tutorial-android.md b/docs/tutorial-android.md index aaa3427e1e..c960001ec1 100644 --- a/docs/tutorial-android.md +++ b/docs/tutorial-android.md @@ -5,7 +5,7 @@ - Add following descriptions to the `dependencies` of your module's `build.gradle`. ``` - implementation "com.ricoh360.thetaclient:theta-client:1.10.2" + implementation "com.ricoh360.thetaclient:theta-client:1.11.0" ``` - Connect the wireless LAN between THETA and the smartphone that runs on the application using this SDK. diff --git a/flutter/android/build.gradle b/flutter/android/build.gradle index 7370745a6c..4efa5ef423 100644 --- a/flutter/android/build.gradle +++ b/flutter/android/build.gradle @@ -53,5 +53,5 @@ dependencies { implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.9") implementation("com.soywiz.korlibs.krypto:krypto:4.0.10") - implementation("com.ricoh360.thetaclient:theta-client:1.10.2") + implementation("com.ricoh360.thetaclient:theta-client:1.11.0") } diff --git a/flutter/android/src/main/kotlin/com/ricoh360/thetaclient/theta_client_flutter/ConvertUtil.kt b/flutter/android/src/main/kotlin/com/ricoh360/thetaclient/theta_client_flutter/ConvertUtil.kt index 58bb132b2e..9fad0955eb 100644 --- a/flutter/android/src/main/kotlin/com/ricoh360/thetaclient/theta_client_flutter/ConvertUtil.kt +++ b/flutter/android/src/main/kotlin/com/ricoh360/thetaclient/theta_client_flutter/ConvertUtil.kt @@ -12,6 +12,7 @@ const val KEY_NOTIFY_PARAM_COMPLETION = "completion" const val KEY_NOTIFY_PARAM_IMAGE = "image" const val KEY_NOTIFY_PARAM_MESSAGE = "message" const val KEY_NOTIFY_PARAM_STATUS = "status" +const val KEY_NOTIFY_PARAM_FILE_URL = "fileUrl" const val KEY_GPS_INFO = "gpsInfo" const val KEY_STATE_EXTERNAL_GPS_INFO = "externalGpsInfo" const val KEY_STATE_INTERNAL_GPS_INFO = "internalGpsInfo" @@ -734,6 +735,7 @@ fun getOptionValueEnum(name: OptionNameEnum, valueName: String): Any? { OptionNameEnum.BurstMode -> BurstModeEnum.values().find { it.name == valueName } OptionNameEnum.CameraControlSource -> CameraControlSourceEnum.values().find { it.name == valueName } OptionNameEnum.CameraMode -> CameraModeEnum.values().find { it.name == valueName } + OptionNameEnum.CameraPower -> CameraPowerEnum.values().find { it.name == valueName } OptionNameEnum.CaptureMode -> CaptureModeEnum.values().find { it.name == valueName } OptionNameEnum.ContinuousNumber -> ContinuousNumberEnum.values().find { it.name == valueName } OptionNameEnum.ExposureCompensation -> ExposureCompensationEnum.values().find { it.name == valueName } @@ -928,3 +930,9 @@ fun toCapturingNotifyParam(status: CapturingStatusEnum): Map { KEY_NOTIFY_PARAM_STATUS to status.name ) } + +fun toStartedNotifyParam(value: String): Map { + return mapOf( + KEY_NOTIFY_PARAM_FILE_URL to value + ) +} diff --git a/flutter/android/src/main/kotlin/com/ricoh360/thetaclient/theta_client_flutter/ThetaClientFlutterPlugin.kt b/flutter/android/src/main/kotlin/com/ricoh360/thetaclient/theta_client_flutter/ThetaClientFlutterPlugin.kt index 8ef714e42f..eb44ddd50b 100644 --- a/flutter/android/src/main/kotlin/com/ricoh360/thetaclient/theta_client_flutter/ThetaClientFlutterPlugin.kt +++ b/flutter/android/src/main/kotlin/com/ricoh360/thetaclient/theta_client_flutter/ThetaClientFlutterPlugin.kt @@ -60,6 +60,7 @@ class ThetaClientFlutterPlugin : FlutterPlugin, MethodCallHandler { const val messageNotInit: String = "Not initialized." const val messageNoResult: String = "Result is Null." const val messageNoArgument: String = "No Argument." + const val messageLivePreviewRunning: String = "Live preview is running." const val eventNameNotify = "theta_client_flutter/theta_notify" const val notifyIdLivePreview = 10001 @@ -85,6 +86,7 @@ class ThetaClientFlutterPlugin : FlutterPlugin, MethodCallHandler { const val notifyIdPhotoCapturing = 10071 const val notifyIdVideoCaptureStopError = 10081 const val notifyIdVideoCaptureCapturing = 10082 + const val notifyIdVideoCaptureStarted = 10083 } fun sendNotifyEvent(id: Int, params: Map) { @@ -616,6 +618,11 @@ class ThetaClientFlutterPlugin : FlutterPlugin, MethodCallHandler { } suspend fun getLivePreview(result: Result) { + if (previewing) { + result.error(errorCode, messageLivePreviewRunning, null) + return + } + val theta = thetaRepository if (theta == null) { result.error(errorCode, messageNotInit, null) @@ -632,13 +639,14 @@ class ThetaClientFlutterPlugin : FlutterPlugin, MethodCallHandler { } result.success(null) } catch (e: Exception) { + previewing = false result.error(e.javaClass.simpleName, e.message, null) } } fun stopLivePreview(result: Result) { previewing = false - result.success(null) + result.success(true) } fun getPhotoCaptureBuilder(result: Result) { @@ -819,6 +827,13 @@ class ThetaClientFlutterPlugin : FlutterPlugin, MethodCallHandler { toCapturingNotifyParam(status) ) } + + override fun onCaptureStarted(fileUrl: String?) { + sendNotifyEvent( + notifyIdVideoCaptureStarted, + toStartedNotifyParam(fileUrl ?: "") + ) + } }) } diff --git a/flutter/ios/Classes/ConvertUtil.swift b/flutter/ios/Classes/ConvertUtil.swift index 17f678fbe8..52b41257ec 100644 --- a/flutter/ios/Classes/ConvertUtil.swift +++ b/flutter/ios/Classes/ConvertUtil.swift @@ -9,6 +9,7 @@ let KEY_NOTIFY_PARAM_COMPLETION = "completion" let KEY_NOTIFY_PARAM_IMAGE = "image" let KEY_NOTIFY_PARAM_MESSAGE = "message" let KEY_NOTIFY_PARAM_STATUS = "status" +let KEY_NOTIFY_PARAM_FILE_URL = "fileUrl" let KEY_GPS_INFO = "gpsInfo" let KEY_STATE_EXTERNAL_GPS_INFO = "externalGpsInfo" let KEY_STATE_INTERNAL_GPS_INFO = "internalGpsInfo" @@ -863,6 +864,8 @@ func setOptionsValue(options: ThetaRepository.Options, name: String, value: Any) options.cameraControlSource = getEnumValue(values: ThetaRepository.CameraControlSourceEnum.values(), name: value as! String)! case ThetaRepository.OptionNameEnum.cameramode.name: options.cameraMode = getEnumValue(values: ThetaRepository.CameraModeEnum.values(), name: value as! String)! + case ThetaRepository.OptionNameEnum.camerapower.name: + options.cameraPower = getEnumValue(values: ThetaRepository.CameraPowerEnum.values(), name: value as! String)! case ThetaRepository.OptionNameEnum.captureinterval.name: options.captureInterval = KotlinInt(integerLiteral: value as! Int) case ThetaRepository.OptionNameEnum.capturemode.name: @@ -1126,3 +1129,9 @@ func toCapturingNotifyParam(value: CapturingStatusEnum) -> [String: Any] { KEY_NOTIFY_PARAM_STATUS: value.name, ] } + +func toStartedNotifyParam(value: String) -> [String: Any] { + return [ + KEY_NOTIFY_PARAM_FILE_URL: value, + ] +} diff --git a/flutter/ios/Classes/SwiftThetaClientFlutterPlugin.swift b/flutter/ios/Classes/SwiftThetaClientFlutterPlugin.swift index d4cf66a3a4..0a310a300a 100644 --- a/flutter/ios/Classes/SwiftThetaClientFlutterPlugin.swift +++ b/flutter/ios/Classes/SwiftThetaClientFlutterPlugin.swift @@ -26,6 +26,7 @@ let NOTIFY_CONTINUOUS_CAPTURING = 10062 let NOTIFY_PHOTO_CAPTURING = 10071 let NOTIFY_VIDEO_CAPTURE_STOP_ERROR = 10081 let NOTIFY_VIDEO_CAPTURE_CAPTURING = 10082 +let NOTIFY_VIDEO_CAPTURE_STARTED = 10083 public class SwiftThetaClientFlutterPlugin: NSObject, FlutterPlugin, FlutterStreamHandler { public func onListen(withArguments _: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { @@ -43,6 +44,7 @@ public class SwiftThetaClientFlutterPlugin: NSObject, FlutterPlugin, FlutterStre static let messageNotInit: String = "Not initialized." static let messageNoResult: String = "Result is Null." static let messageNoArgument: String = "No Argument." + static let messageLivePreviewRunning = "Live preview is running." static var endPoint: String = "http://192.168.1.1" var eventSink: FlutterEventSink? = nil var previewing = false @@ -113,7 +115,7 @@ public class SwiftThetaClientFlutterPlugin: NSObject, FlutterPlugin, FlutterStre case "getLivePreview": getLivePreview(result: result) case "stopLivePreview": - previewing = false + stopLivePreview(result: result) case "listFiles": listFiles(call: call, result: result) case "deleteFiles": @@ -382,6 +384,12 @@ public class SwiftThetaClientFlutterPlugin: NSObject, FlutterPlugin, FlutterStre } func getLivePreview(result: @escaping FlutterResult) { + if previewing { + let flutterError = FlutterError(code: SwiftThetaClientFlutterPlugin.errorCode, message: SwiftThetaClientFlutterPlugin.messageLivePreviewRunning, details: nil) + result(flutterError) + return + } + if thetaRepository == nil { let flutterError = FlutterError(code: SwiftThetaClientFlutterPlugin.errorCode, message: SwiftThetaClientFlutterPlugin.messageNotInit, details: nil) result(flutterError) @@ -417,6 +425,11 @@ public class SwiftThetaClientFlutterPlugin: NSObject, FlutterPlugin, FlutterStre } } } + + func stopLivePreview(result: @escaping FlutterResult) { + previewing = false + result(true) + } func listFiles(call: FlutterMethodCall, result: @escaping FlutterResult) { guard let thetaRepository = thetaRepository else { @@ -738,6 +751,10 @@ public class SwiftThetaClientFlutterPlugin: NSObject, FlutterPlugin, FlutterStre func onCapturing(status: CapturingStatusEnum) { plugin?.sendNotifyEvent(id: NOTIFY_VIDEO_CAPTURE_CAPTURING, params: toCapturingNotifyParam(value: status)) } + + func onCaptureStarted(fileUrl: String?) { + plugin?.sendNotifyEvent(id: NOTIFY_VIDEO_CAPTURE_STARTED, params: toStartedNotifyParam(value: fileUrl ?? "")) + } } videoCapturing = videoCapture!.startCapture( callback: Callback({ fileUrl, error in diff --git a/flutter/ios/theta_client_flutter.podspec b/flutter/ios/theta_client_flutter.podspec index eaa0c52c6c..2290e08c5e 100644 --- a/flutter/ios/theta_client_flutter.podspec +++ b/flutter/ios/theta_client_flutter.podspec @@ -4,7 +4,7 @@ # Pod::Spec.new do |s| s.name = 'theta_client_flutter' - s.version = '1.10.2' + s.version = '1.11.0' s.summary = 'theta-client plugin project.' s.description = <<-DESC theta-client Flutter plugin project. @@ -17,7 +17,7 @@ Pod::Spec.new do |s| s.dependency 'Flutter' s.platform = :ios, '15.0' - s.dependency 'THETAClient', '1.10.2' + s.dependency 'THETAClient', '1.11.0' # Flutter.framework does not contain a i386 slice. s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } diff --git a/flutter/lib/capture/capture.dart b/flutter/lib/capture/capture.dart index 2278e5e3ff..5cca4d932e 100644 --- a/flutter/lib/capture/capture.dart +++ b/flutter/lib/capture/capture.dart @@ -171,9 +171,10 @@ class VideoCapture extends Capture { VideoCapturing startCapture(void Function(String? fileUrl) onCaptureCompleted, void Function(Exception exception) onCaptureFailed, {void Function(Exception exception)? onStopFailed, - void Function(CapturingStatusEnum status)? onCapturing}) { + void Function(CapturingStatusEnum status)? onCapturing, + void Function(String? fileUrl)? onCaptureStarted}) { ThetaClientFlutterPlatform.instance - .startVideoCapture(onStopFailed, onCapturing) + .startVideoCapture(onStopFailed, onCapturing, onCaptureStarted) .then((value) => onCaptureCompleted(value)) .onError((error, stackTrace) => onCaptureFailed(error as Exception)); return VideoCapturing(); diff --git a/flutter/lib/options/camera_power.dart b/flutter/lib/options/camera_power.dart new file mode 100644 index 0000000000..4824ad6656 --- /dev/null +++ b/flutter/lib/options/camera_power.dart @@ -0,0 +1,36 @@ +/// _cameraPower is the power status of camera. +/// +/// For RICOH THETA X v2.61.0 or later +enum CameraPowerEnum { + /// Undefined value + unknown('UNKNOWN'), + + /// Power ON + on('ON'), + + /// Power OFF + off('OFF'), + + /// Power on, power saving mode. Camera is closed. + /// Unavailable parameter when plugin is running. In this case, invalidParameterValue error will be returned. + powerSaving('POWER_SAVING'), + + /// Power on, silent mode. LCD/LED is turned off. + /// Unavailable parameter when plugin is running. In this case, invalidParameterValue error will be returned. + silentMode('SILENT_MODE'); + + final String rawValue; + + const CameraPowerEnum(this.rawValue); + + @override + String toString() { + return rawValue; + } + + static CameraPowerEnum? getValue(String rawValue) { + return CameraPowerEnum.values.cast().firstWhere( + (element) => element?.rawValue == rawValue, + orElse: () => null); + } +} diff --git a/flutter/lib/options/importer.dart b/flutter/lib/options/importer.dart index 19129cf2d4..fc0f5447f1 100644 --- a/flutter/lib/options/importer.dart +++ b/flutter/lib/options/importer.dart @@ -2,6 +2,7 @@ library; export 'bluetooth_role.dart'; +export 'camera_power.dart'; export 'ethernet_config.dart'; export 'file_format.dart'; export 'max_recordable_time.dart'; diff --git a/flutter/lib/theta_client_flutter.dart b/flutter/lib/theta_client_flutter.dart index 54a6352ea5..6b15b172b5 100644 --- a/flutter/lib/theta_client_flutter.dart +++ b/flutter/lib/theta_client_flutter.dart @@ -1239,6 +1239,9 @@ enum OptionNameEnum { /// Option name _cameraMode cameraMode('CameraMode', CameraModeEnum), + /// Option name _cameraPower + cameraPower('CameraPower', CameraPowerEnum), + /// Option name captureInterval captureInterval('CaptureInterval', int), @@ -3253,6 +3256,9 @@ class Options { /// For RICOH THETA X CameraModeEnum? cameraMode; + /// see [CameraPowerEnum] + CameraPowerEnum? cameraPower; + /// Shooting interval (sec.) for interval shooting. /// /// ### Support value @@ -3526,6 +3532,8 @@ class Options { return cameraControlSource as T; case OptionNameEnum.cameraMode: return cameraMode as T; + case OptionNameEnum.cameraPower: + return cameraPower as T; case OptionNameEnum.captureInterval: return captureInterval as T; case OptionNameEnum.captureMode: @@ -3664,6 +3672,9 @@ class Options { case OptionNameEnum.cameraMode: cameraMode = value; break; + case OptionNameEnum.cameraPower: + cameraPower = value; + break; case OptionNameEnum.captureInterval: captureInterval = value; break; diff --git a/flutter/lib/theta_client_flutter_method_channel.dart b/flutter/lib/theta_client_flutter_method_channel.dart index ccd0760e08..357f7223bb 100644 --- a/flutter/lib/theta_client_flutter_method_channel.dart +++ b/flutter/lib/theta_client_flutter_method_channel.dart @@ -30,6 +30,7 @@ const notifyIdContinuousCaptureCapturing = 10062; const notifyIdPhotoCapturing = 10071; const notifyIdVideoCaptureStopError = 10081; const notifyIdVideoCaptureCapturing = 10082; +const notifyIdVideoCaptureStarted = 10083; /// An implementation of [ThetaClientFlutterPlatform] that uses method channels. class MethodChannelThetaClientFlutter extends ThetaClientFlutterPlatform { @@ -181,14 +182,18 @@ class MethodChannelThetaClientFlutter extends ThetaClientFlutterPlatform { final image = params?['image'] as Uint8List?; if (image != null && !frameHandler(image)) { removeNotify(notifyIdLivePreview); - methodChannel.invokeMethod('stopLivePreview'); + methodChannel.invokeMethod('stopLivePreview'); } }); await methodChannel.invokeMethod('getLivePreview'); completer.complete(null); } catch (e) { - removeNotify(notifyIdLivePreview); - completer.completeError(e); + if (e is PlatformException && e.message == "Live preview is running.") { + completer.completeError(e); + } else { + removeNotify(notifyIdLivePreview); + completer.completeError(e); + } } return completer.future; } @@ -360,7 +365,8 @@ class MethodChannelThetaClientFlutter extends ThetaClientFlutterPlatform { @override Future startVideoCapture( void Function(Exception exception)? onStopFailed, - void Function(CapturingStatusEnum status)? onCapturing) async { + void Function(CapturingStatusEnum status)? onCapturing, + void Function(String? fileUrl)? onCaptureStarted) async { var completer = Completer(); try { enableNotifyEventReceiver(); @@ -383,6 +389,12 @@ class MethodChannelThetaClientFlutter extends ThetaClientFlutterPlatform { } }); } + if (onCaptureStarted != null) { + addNotify(notifyIdVideoCaptureStarted, (params) { + final fileUrl = params?['fileUrl'] as String?; + onCaptureStarted(fileUrl?.isNotEmpty == true ? fileUrl : null); + }); + } final fileUrl = await methodChannel.invokeMethod('startVideoCapture'); removeNotify(notifyIdVideoCaptureStopError); diff --git a/flutter/lib/theta_client_flutter_platform_interface.dart b/flutter/lib/theta_client_flutter_platform_interface.dart index 382f7e7d66..38bc015f21 100644 --- a/flutter/lib/theta_client_flutter_platform_interface.dart +++ b/flutter/lib/theta_client_flutter_platform_interface.dart @@ -135,7 +135,8 @@ abstract class ThetaClientFlutterPlatform extends PlatformInterface { Future startVideoCapture( void Function(Exception exception)? onStopFailed, - void Function(CapturingStatusEnum status)? onCapturing) { + void Function(CapturingStatusEnum status)? onCapturing, + void Function(String? fileUrl)? onCaptureStarted) { throw UnimplementedError('startVideoCapture() has not been implemented.'); } diff --git a/flutter/lib/utils/convert_utils.dart b/flutter/lib/utils/convert_utils.dart index 728255f0f8..78af22e5fa 100644 --- a/flutter/lib/utils/convert_utils.dart +++ b/flutter/lib/utils/convert_utils.dart @@ -431,6 +431,9 @@ class ConvertUtils { case OptionNameEnum.cameraMode: result.cameraMode = CameraModeEnum.getValue(entry.value); break; + case OptionNameEnum.cameraPower: + result.cameraPower = CameraPowerEnum.getValue(entry.value); + break; case OptionNameEnum.captureInterval: result.captureInterval = entry.value; break; @@ -624,6 +627,8 @@ class ConvertUtils { return value.rawValue; } else if (value is CameraModeEnum) { return value.rawValue; + } else if (value is CameraPowerEnum) { + return value.rawValue; } else if (value is CaptureModeEnum) { return value.rawValue; } else if (value is ContinuousNumberEnum) { diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 1a8a633ef2..16bb95bdeb 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -1,6 +1,6 @@ name: theta_client_flutter description: THETA Client Flutter plugin project. -version: 1.10.2 +version: 1.11.0 homepage: environment: diff --git a/flutter/test/capture/video_capture_method_channel_test.dart b/flutter/test/capture/video_capture_method_channel_test.dart index 11dd6eb34e..c573d0fb77 100644 --- a/flutter/test/capture/video_capture_method_channel_test.dart +++ b/flutter/test/capture/video_capture_method_channel_test.dart @@ -77,7 +77,7 @@ void main() { .setMockMethodCallHandler(channel, (MethodCall methodCall) async { return fileUrl; }); - expect(await platform.startVideoCapture(null, null), fileUrl); + expect(await platform.startVideoCapture(null, null, null), fileUrl); }); test('call onStopFailed', () async { @@ -103,7 +103,7 @@ void main() { expect( await platform.startVideoCapture((exception) { isOnStopFailed = true; - }, null), + }, null, null), fileUrl); expect(platform.notifyList.containsKey(10081), false, reason: 'remove notify stop error'); @@ -133,10 +133,40 @@ void main() { expect( await platform.startVideoCapture(null, (status) { lastStatus = status; - }), + }, null), fileUrl); expect(platform.notifyList.containsKey(10082), false, reason: 'remove notify capturing status'); expect(lastStatus, CapturingStatusEnum.selfTimerCountdown); }); + + test('call onCaptureStarted', () async { + const fileUrl = + 'http://192.168.1.1/files/150100524436344d4201375fda9dc400/100RICOH/R0013336.MP4'; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { + expect(platform.notifyList.containsKey(10083), true, + reason: 'add notify stop error'); + + // native event + platform.onNotify({ + 'id': 10083, + 'params': { + 'fileUrl': "xxx.mp4", + }, + }); + + return fileUrl; + }); + + String? thisTimeFileUrl; + expect( + await platform.startVideoCapture(null, null, (fileUrl) { + thisTimeFileUrl = fileUrl; + }), + fileUrl); + expect(platform.notifyList.containsKey(10083), true, + reason: 'capture started'); + expect(thisTimeFileUrl, "xxx.mp4"); + }); } diff --git a/flutter/test/options/option_camera_power_test.dart b/flutter/test/options/option_camera_power_test.dart new file mode 100644 index 0000000000..3b46f6d040 --- /dev/null +++ b/flutter/test/options/option_camera_power_test.dart @@ -0,0 +1,58 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:theta_client_flutter/theta_client_flutter.dart'; +import 'package:theta_client_flutter/theta_client_flutter_method_channel.dart'; + +void main() { + MethodChannelThetaClientFlutter platform = MethodChannelThetaClientFlutter(); + const MethodChannel channel = MethodChannel('theta_client_flutter'); + + TestWidgetsFlutterBinding.ensureInitialized(); + + setUp(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, null); + }); + + tearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, null); + }); + + test('CameraPowerEnum const', () async { + List> data = [ + [CameraPowerEnum.unknown, 'UNKNOWN'], + [CameraPowerEnum.on, 'ON'], + [CameraPowerEnum.off, 'OFF'], + [CameraPowerEnum.powerSaving, 'POWER_SAVING'], + [CameraPowerEnum.silentMode, 'SILENT_MODE'] + ]; + expect(data.length, CameraPowerEnum.values.length, reason: 'enum count'); + for (int i = 0; i < data.length; i++) { + expect(data[i][0].toString(), data[i][1], reason: data[i][1]); + } + }); + + test('getOptions', () async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { + Map optionMap = {}; + optionMap['CameraPower'] = 'SILENT_MODE'; + return Future.value(optionMap); + }); + Options options = await platform.getOptions([OptionNameEnum.cameraPower]); + expect(options.cameraPower?.rawValue, 'SILENT_MODE', reason: 'quality'); + }); + + test('setOptions', () async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { + var arguments = methodCall.arguments as Map; + expect(arguments['CameraPower'], 'POWER_SAVING', reason: 'quality'); + return Future.value(); + }); + final options = Options(); + options.cameraPower = CameraPowerEnum.powerSaving; + await platform.setOptions(options); + }); +} diff --git a/flutter/test/theta_client_flutter_test.dart b/flutter/test/theta_client_flutter_test.dart index 2221606763..b3a30e6247 100644 --- a/flutter/test/theta_client_flutter_test.dart +++ b/flutter/test/theta_client_flutter_test.dart @@ -82,7 +82,8 @@ class MockThetaClientFlutterPlatform } @override - Future startTimeShiftCapture(void Function(double)? onProgress, + Future startTimeShiftCapture( + void Function(double)? onProgress, void Function(Exception exception)? onStopFailed, void Function(CapturingStatusEnum status)? onCapturing) { return onCallStartTimeShiftCapture(onProgress, onStopFailed, onCapturing); @@ -106,7 +107,8 @@ class MockThetaClientFlutterPlatform @override Future startVideoCapture( void Function(Exception exception)? onStopFailed, - void Function(CapturingStatusEnum status)? onCapturing) { + void Function(CapturingStatusEnum status)? onCapturing, + void Function(String? fileUrl)? onCaptureStarted) { return onCallStartVideoCapture(onStopFailed, onCapturing); } @@ -116,8 +118,8 @@ class MockThetaClientFlutterPlatform } @override - Future buildLimitlessIntervalCapture(Map options, - int interval) { + Future buildLimitlessIntervalCapture( + Map options, int interval) { return onCallBuildLimitlessIntervalCapture(options, interval); } @@ -432,19 +434,20 @@ Future Function() onCallGetLivePreview = Future.value; Future Function() onCallListFiles = Future.value; Future Function() onCallGetPhotoCaptureBuilder = Future.value; -Future Function(Map options, int interval) onCallBuildPhotoCapture = - (options, interval) => Future.value(); +Future Function(Map options, int interval) + onCallBuildPhotoCapture = (options, interval) => Future.value(); Future Function(void Function(CapturingStatusEnum)? onCapturing) onCallTakePicture = (onCapturing) => Future.value(); Future Function() onCallGetTimeShiftCaptureBuilder = Future.value; Future Function(Map options, int interval) -onCallBuildTimeShiftCapture = (options, interval) => Future.value(); -Future Function(void Function(double)? onProgress, - void Function(Exception exception)? onStopFailed, - void Function(CapturingStatusEnum status)? onCapturing) -onCallStartTimeShiftCapture = (onProgress, onStopFailed, onCapturing) => - Future.value(); + onCallBuildTimeShiftCapture = (options, interval) => Future.value(); +Future Function( + void Function(double)? onProgress, + void Function(Exception exception)? onStopFailed, + void Function(CapturingStatusEnum status)? onCapturing) + onCallStartTimeShiftCapture = + (onProgress, onStopFailed, onCapturing) => Future.value(); Future Function() onCallStopTimeShiftCapture = Future.value; Future Function() onCallGetVideoCaptureBuilder = Future.value; @@ -457,34 +460,36 @@ Future Function() onCallStopVideoCapture = Future.value; Future Function() onCallGetLimitlessIntervalCaptureBuilder = Future.value; Future Function(Map options, int interval) -onCallBuildLimitlessIntervalCapture = (options, interval) => Future.value(); + onCallBuildLimitlessIntervalCapture = (options, interval) => Future.value(); Future?> Function(void Function(Exception exception)? onStopFailed, - void Function(CapturingStatusEnum status)? onCapturing) -onCallStartLimitlessIntervalCapture = (onStopFailed, onCapturing) => - Future.value(); + void Function(CapturingStatusEnum status)? onCapturing) + onCallStartLimitlessIntervalCapture = + (onStopFailed, onCapturing) => Future.value(); Future Function() onCallStopLimitlessIntervalCapture = Future.value; Future Function(int shotCount) -onCallGetShotCountSpecifiedIntervalCaptureBuilder = Future.value; + onCallGetShotCountSpecifiedIntervalCaptureBuilder = Future.value; Future Function(Map options, int interval) -onCallBuildShotCountSpecifiedIntervalCapture = + onCallBuildShotCountSpecifiedIntervalCapture = (options, interval) => Future.value(); -Future?> Function(void Function(double)? onProgress, - void Function(Exception exception)? onStopFailed, - void Function(CapturingStatusEnum status)? onCapturing) -onCallStartShotCountSpecifiedIntervalCapture = +Future?> Function( + void Function(double)? onProgress, + void Function(Exception exception)? onStopFailed, + void Function(CapturingStatusEnum status)? onCapturing) + onCallStartShotCountSpecifiedIntervalCapture = (onProgress, onStopFailed, onCapturing) => Future.value(); Future Function() onCallStopShotCountSpecifiedIntervalCapture = Future.value; Future Function(int shootingTimeSec) -onCallGetCompositeIntervalCaptureBuilder = Future.value; + onCallGetCompositeIntervalCaptureBuilder = Future.value; Future Function(Map options, int interval) -onCallBuildCompositeIntervalCapture = (options, interval) => Future.value(); -Future?> Function(void Function(double)? onProgress, - void Function(Exception exception)? onStopFailed, - void Function(CapturingStatusEnum status)? onCapturing) -onCallStartCompositeIntervalCapture = + onCallBuildCompositeIntervalCapture = (options, interval) => Future.value(); +Future?> Function( + void Function(double)? onProgress, + void Function(Exception exception)? onStopFailed, + void Function(CapturingStatusEnum status)? onCapturing) + onCallStartCompositeIntervalCapture = (onProgress, onStopFailed, onCapturing) => Future.value(); Future Function() onCallStopCompositeIntervalCapture = Future.value; @@ -512,10 +517,10 @@ Future Function() onCallStopMultiBracketCapture = Future.value; Future Function() onCallGetContinuousCaptureBuilder = Future.value; Future Function(Map options, int interval) -onCallBuildContinuousCapture = (options, interval) => Future.value(); + onCallBuildContinuousCapture = (options, interval) => Future.value(); Future?> Function(void Function(double)? onProgress, - void Function(CapturingStatusEnum status)? onCapturing) -onCallStartContinuousCapture = (onProgress, onCapturing) => Future.value(); + void Function(CapturingStatusEnum status)? onCapturing) + onCallStartContinuousCapture = (onProgress, onCapturing) => Future.value(); Future Function(List optionNames) onCallGetOptions = (optionNames) => Future.value(Options()); diff --git a/kotlin-multiplatform/build.gradle.kts b/kotlin-multiplatform/build.gradle.kts index 43dd3ec8dd..8b2e6ba2f4 100644 --- a/kotlin-multiplatform/build.gradle.kts +++ b/kotlin-multiplatform/build.gradle.kts @@ -17,7 +17,7 @@ dependencies { dokkaPlugin("org.jetbrains.dokka:versioning-plugin:1.9.10") } -val thetaClientVersion = "1.10.2" +val thetaClientVersion = "1.11.0" group = "com.ricoh360.thetaclient" version = thetaClientVersion diff --git a/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/PreviewClient.kt b/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/PreviewClient.kt index 2f1aef57af..b2b5227304 100644 --- a/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/PreviewClient.kt +++ b/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/PreviewClient.kt @@ -260,8 +260,14 @@ internal class PreviewClientImpl : PreviewClient { */ private suspend fun fillBuffer(): Int { pos = 0 - curr = input!!.readAvailable(buffer, 0, buffer.size) - return curr + try { + return withTimeout(socketTimeout) { + curr = input!!.readAvailable(buffer, 0, buffer.size) + curr + } + } catch (t: Throwable) { + throw PreviewClientException(t.message ?: "readAvailable error", t) + } } /** diff --git a/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/ThetaRepository.kt b/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/ThetaRepository.kt index cee22ef2b1..c27d523db8 100644 --- a/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/ThetaRepository.kt +++ b/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/ThetaRepository.kt @@ -756,6 +756,12 @@ class ThetaRepository internal constructor(val endpoint: String, config: Config? */ CameraMode("_cameraMode", CameraModeEnum::class), + /** + * Option name + * _cameraPower + */ + CameraPower("_cameraPower", CameraPowerEnum::class), + /** * Option name * captureInterval @@ -1121,6 +1127,11 @@ class ThetaRepository internal constructor(val endpoint: String, config: Config? */ var cameraMode: CameraModeEnum? = null, + /** + * @see CameraPowerEnum + */ + var cameraPower: CameraPowerEnum? = null, + /** * Shooting interval (sec.) for interval shooting. * @@ -1484,6 +1495,7 @@ class ThetaRepository internal constructor(val endpoint: String, config: Config? burstOption = null, cameraControlSource = null, cameraMode = null, + cameraPower = null, captureInterval = null, captureMode = null, captureNumber = null, @@ -1546,6 +1558,7 @@ class ThetaRepository internal constructor(val endpoint: String, config: Config? burstOption = options._burstOption?.let { BurstOption(it) }, cameraControlSource = options._cameraControlSource?.let { CameraControlSourceEnum.get(it) }, cameraMode = options._cameraMode?.let { CameraModeEnum.get(it) }, + cameraPower = options._cameraPower?.let { CameraPowerEnum.get(it) }, captureInterval = options.captureInterval, captureMode = options.captureMode?.let { CaptureModeEnum.get(it) }, captureNumber = options.captureNumber, @@ -1617,6 +1630,7 @@ class ThetaRepository internal constructor(val endpoint: String, config: Config? _burstOption = burstOption?.toTransferredBurstOption(), _cameraControlSource = cameraControlSource?.value, _cameraMode = cameraMode?.value, + _cameraPower = cameraPower?.value, captureInterval = captureInterval, captureMode = captureMode?.value, captureNumber = captureNumber, @@ -1691,6 +1705,7 @@ class ThetaRepository internal constructor(val endpoint: String, config: Config? OptionNameEnum.BurstOption -> burstOption OptionNameEnum.CameraControlSource -> cameraControlSource OptionNameEnum.CameraMode -> cameraMode + OptionNameEnum.CameraPower -> cameraPower OptionNameEnum.CaptureInterval -> captureInterval OptionNameEnum.CaptureMode -> captureMode OptionNameEnum.CaptureNumber -> captureNumber @@ -1766,6 +1781,7 @@ class ThetaRepository internal constructor(val endpoint: String, config: Config? OptionNameEnum.BurstOption -> burstOption = value as BurstOption OptionNameEnum.CameraControlSource -> cameraControlSource = value as CameraControlSourceEnum OptionNameEnum.CameraMode -> cameraMode = value as CameraModeEnum + OptionNameEnum.CameraPower -> cameraPower = value as CameraPowerEnum OptionNameEnum.CaptureInterval -> captureInterval = value as Int OptionNameEnum.CaptureMode -> captureMode = value as CaptureModeEnum OptionNameEnum.CaptureNumber -> captureNumber = value as Int @@ -2623,6 +2639,52 @@ class ThetaRepository internal constructor(val endpoint: String, config: Config? } } + /** + * _cameraPower is the power status of camera. + * + * For RICOH THETA X v2.61.0 or later + */ + enum class CameraPowerEnum(internal val value: CameraPower) { + /** + * Undefined value + */ + UNKNOWN(CameraPower.UNKNOWN), + + /** + * Power ON + */ + ON(CameraPower.ON), + + /** + * Power OFF + */ + OFF(CameraPower.OFF), + + /** + * Power on, power saving mode. Camera is closed. + * Unavailable parameter when plugin is running. In this case, invalidParameterValue error will be returned. + */ + POWER_SAVING(CameraPower.POWER_SAVING), + + /** + * Power on, silent mode. LCD/LED is turned off. + * Unavailable parameter when plugin is running. In this case, invalidParameterValue error will be returned. + */ + SILENT_MODE(CameraPower.SILENT_MODE); + + companion object { + /** + * Convert CameraPower to CameraPowerEnum + * + * @param value CameraPower. + * @return CameraPowerEnum + */ + internal fun get(value: CameraPower): CameraPowerEnum? { + return values().firstOrNull { it.value == value } + } + } + } + /** * Shooting mode. */ diff --git a/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/capture/VideoCapture.kt b/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/capture/VideoCapture.kt index 35b24274ef..4145fd4c26 100644 --- a/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/capture/VideoCapture.kt +++ b/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/capture/VideoCapture.kt @@ -85,6 +85,14 @@ class VideoCapture private constructor( * @param status Capturing status */ fun onCapturing(status: CapturingStatusEnum) {} + + /** + * Called when the capture has already started. + * + * @param fileUrl URL of the video capture. + * Always null or empty when using self-timer. + */ + fun onCaptureStarted(fileUrl: String?) {} } internal suspend fun getCaptureStatus(): CaptureStatus? { @@ -171,7 +179,9 @@ class VideoCapture private constructor( } scope.launch { try { - ThetaApi.callStartCaptureCommand(endpoint, StartCaptureParams()).error?.let { + val response = ThetaApi.callStartCaptureCommand(endpoint, StartCaptureParams()) + callback.onCaptureStarted(fileUrl = response._fileUrls?.firstOrNull()) + response.error?.let { callOnCaptureFailed(ThetaRepository.ThetaWebApiException(it.message)) } } catch (e: JsonConvertException) { diff --git a/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/transferred/setOptionsCommand.kt b/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/transferred/setOptionsCommand.kt index 34286c8ab7..218129c4d3 100644 --- a/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/transferred/setOptionsCommand.kt +++ b/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/transferred/setOptionsCommand.kt @@ -187,6 +187,20 @@ internal data class Options( */ var _cameraMode: CameraMode? = null, + /** + * Camera power + * + * @see CameraPower + */ + var _cameraPower: CameraPower? = null, + + /** + * Camera power support + * + * @see CameraPower + */ + var _cameraPowerSupport: List? = null, + /** * Shooting interval (sec.) for interval shooting. */ @@ -1297,6 +1311,52 @@ internal enum class CameraMode { PLUGIN, } +internal object CameraPowerSerializer : + SerialNameEnumIgnoreUnknownSerializer(CameraPower.entries, CameraPower.UNKNOWN) + +/** + * _cameraPower is the power status of camera. + * + * For RICOH THETA X v2.61.0 or later + */ +@Serializable(with = CameraPowerSerializer::class) +internal enum class CameraPower : SerialNameEnum { + /** + * Undefined value + */ + UNKNOWN, + + /** + * Power ON + */ + ON { + override val serialName: String = "on" + }, + + /** + * Power OFF + */ + OFF { + override val serialName: String = "off" + }, + + /** + * Power on, power saving mode. Camera is closed. + * Unavailable parameter when plugin is running. In this case, invalidParameterValue error will be returned. + */ + POWER_SAVING { + override val serialName: String = "powerSaving" + }, + + /** + * Power on, silent mode. LCD/LED is turned off. + * Unavailable parameter when plugin is running. In this case, invalidParameterValue error will be returned. + */ + SILENT_MODE { + override val serialName: String = "silentMode" + }, +} + /** * capture Mode */ diff --git a/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/transferred/startCaptureCommand.kt b/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/transferred/startCaptureCommand.kt index de21f64566..592ed17381 100644 --- a/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/transferred/startCaptureCommand.kt +++ b/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/transferred/startCaptureCommand.kt @@ -113,6 +113,13 @@ internal data class StartCaptureResponse( * "inProgress" */ override val progress: CommandProgress? = null, + + /** + * List of file URLs. + * + * For RICOH THETA X v2.61.0 or later + */ + val _fileUrls: List? = null, ) : CommandApiResponse /** diff --git a/kotlin-multiplatform/src/commonTest/kotlin/com/ricoh360/thetaclient/CheckRequest.kt b/kotlin-multiplatform/src/commonTest/kotlin/com/ricoh360/thetaclient/CheckRequest.kt index c34100bee7..b90ea15f8c 100644 --- a/kotlin-multiplatform/src/commonTest/kotlin/com/ricoh360/thetaclient/CheckRequest.kt +++ b/kotlin-multiplatform/src/commonTest/kotlin/com/ricoh360/thetaclient/CheckRequest.kt @@ -66,6 +66,7 @@ internal class CheckRequest { burstOption: BurstOption? = null, cameraControlSource: CameraControlSource? = null, cameraMode: CameraMode? = null, + cameraPower: CameraPower? = null, captureInterval: Int? = null, captureMode: CaptureMode? = null, captureNumber: Int? = null, @@ -149,6 +150,9 @@ internal class CheckRequest { cameraMode?.let { assertEquals(optionsRequest.parameters.options._cameraMode, it, "setOptions cameraMode") } + cameraPower?.let { + assertEquals(optionsRequest.parameters.options._cameraPower, it, "setOptions cameraPower") + } captureInterval?.let { assertEquals(optionsRequest.parameters.options.captureInterval, it, "setOptions captureInterval") } diff --git a/kotlin-multiplatform/src/commonTest/kotlin/com/ricoh360/thetaclient/capture/VideoCaptureTest.kt b/kotlin-multiplatform/src/commonTest/kotlin/com/ricoh360/thetaclient/capture/VideoCaptureTest.kt index 896c06f5cb..594eb57740 100644 --- a/kotlin-multiplatform/src/commonTest/kotlin/com/ricoh360/thetaclient/capture/VideoCaptureTest.kt +++ b/kotlin-multiplatform/src/commonTest/kotlin/com/ricoh360/thetaclient/capture/VideoCaptureTest.kt @@ -129,6 +129,11 @@ class VideoCaptureTest { else -> assertEquals(status, CapturingStatusEnum.CAPTURING) } } + + override fun onCaptureStarted(fileUrl: String?) { + assertEquals(fileUrl, "http://192.168.1.1/files/100RICOH/R0010429.MP4", "onCaptureStarted") + deferred.complete(Unit) + } }) runBlocking { withTimeout(10000) { diff --git a/kotlin-multiplatform/src/commonTest/kotlin/com/ricoh360/thetaclient/repository/ThetaRepositoryTest.kt b/kotlin-multiplatform/src/commonTest/kotlin/com/ricoh360/thetaclient/repository/ThetaRepositoryTest.kt index 29c521f9d9..5533cb6112 100644 --- a/kotlin-multiplatform/src/commonTest/kotlin/com/ricoh360/thetaclient/repository/ThetaRepositoryTest.kt +++ b/kotlin-multiplatform/src/commonTest/kotlin/com/ricoh360/thetaclient/repository/ThetaRepositoryTest.kt @@ -284,9 +284,9 @@ class ThetaRepositoryTest { // execute val timeout = ThetaRepository.Timeout( - connectTimeout = 1L, - requestTimeout = 2L, - socketTimeout = 3L + connectTimeout = 10L, + requestTimeout = 20L, + socketTimeout = 30L ) ThetaRepository.newInstance(endpoint, timeout = timeout) assertNotNull(ThetaRepository.restoreConfig, "restoreConfig") diff --git a/kotlin-multiplatform/src/commonTest/kotlin/com/ricoh360/thetaclient/repository/options/CameraPowerTest.kt b/kotlin-multiplatform/src/commonTest/kotlin/com/ricoh360/thetaclient/repository/options/CameraPowerTest.kt new file mode 100644 index 0000000000..9402dbd187 --- /dev/null +++ b/kotlin-multiplatform/src/commonTest/kotlin/com/ricoh360/thetaclient/repository/options/CameraPowerTest.kt @@ -0,0 +1,128 @@ +package com.ricoh360.thetaclient.repository.options + +import com.goncalossilva.resources.Resource +import com.ricoh360.thetaclient.CheckRequest +import com.ricoh360.thetaclient.MockApiClient +import com.ricoh360.thetaclient.ThetaRepository +import com.ricoh360.thetaclient.transferred.CameraPower +import com.ricoh360.thetaclient.transferred.Options +import io.ktor.http.HttpStatusCode +import io.ktor.utils.io.ByteReadChannel +import kotlinx.coroutines.test.runTest +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals + +class CameraPowerTest { + private val endpoint = "http://192.168.1.1:80/" + + @BeforeTest + fun setup() { + MockApiClient.status = HttpStatusCode.OK + } + + @AfterTest + fun teardown() { + MockApiClient.status = HttpStatusCode.OK + } + + /** + * Get option _cameraPower. + */ + @Test + fun getOptionCameraPowerTest() = runTest { + val optionNames = listOf( + ThetaRepository.OptionNameEnum.CameraPower + ) + val stringOptionNames = listOf( + "_cameraPower" + ) + + MockApiClient.onRequest = { request -> + // check request + CheckRequest.checkGetOptions(request, stringOptionNames) + + ByteReadChannel(Resource("src/commonTest/resources/options/option_camera_power_saving.json").readText()) + } + + val thetaRepository = ThetaRepository(endpoint) + val options = thetaRepository.getOptions(optionNames) + assertEquals(options.cameraPower, ThetaRepository.CameraPowerEnum.POWER_SAVING) + } + + /** + * Get option _cameraPower UNKNOWN. + */ + @Test + fun getOptionCameraPowerUnknownTest() = runTest { + val optionNames = listOf( + ThetaRepository.OptionNameEnum.CameraPower + ) + val stringOptionNames = listOf( + "_cameraPower" + ) + + MockApiClient.onRequest = { request -> + // check request + CheckRequest.checkGetOptions(request, stringOptionNames) + + ByteReadChannel(Resource("src/commonTest/resources/options/option_camera_unknown.json").readText()) + } + + val thetaRepository = ThetaRepository(endpoint) + val options = thetaRepository.getOptions(optionNames) + assertEquals(options.cameraPower, ThetaRepository.CameraPowerEnum.UNKNOWN) + } + + /** + * Set option _cameraPower. + */ + @Test + fun setOptionCameraPowerTest() = runTest { + val value = Pair(ThetaRepository.CameraPowerEnum.SILENT_MODE, CameraPower.SILENT_MODE) + + MockApiClient.onRequest = { request -> + // check request + CheckRequest.checkSetOptions(request, cameraPower = value.second) + + ByteReadChannel(Resource("src/commonTest/resources/setOptions/set_options_done.json").readText()) + } + + val thetaRepository = ThetaRepository(endpoint) + val options = ThetaRepository.Options( + cameraPower = value.first + ) + thetaRepository.setOptions(options) + } + + /** + * Convert ThetaRepository.Options to Options. + */ + @Test + fun convertOptionCameraPowerTest() = runTest { + val values = listOf( + Pair(ThetaRepository.CameraPowerEnum.UNKNOWN, CameraPower.UNKNOWN), + Pair(ThetaRepository.CameraPowerEnum.ON, CameraPower.ON), + Pair(ThetaRepository.CameraPowerEnum.OFF, CameraPower.OFF), + Pair(ThetaRepository.CameraPowerEnum.POWER_SAVING, CameraPower.POWER_SAVING), + Pair(ThetaRepository.CameraPowerEnum.SILENT_MODE, CameraPower.SILENT_MODE), + ) + + values.forEach { + val orgOptions = Options( + _cameraPower = it.second + ) + val options = ThetaRepository.Options(orgOptions) + assertEquals(options.cameraPower, it.first, "cameraPower ${it.second}") + } + + values.forEach { + val orgOptions = ThetaRepository.Options( + cameraPower = it.first + ) + val options = orgOptions.toOptions() + assertEquals(options._cameraPower, it.second, "_cameraPower ${it.second}") + } + } +} \ No newline at end of file diff --git a/kotlin-multiplatform/src/commonTest/kotlin/com/ricoh360/thetaclient/repository/options/OptionsTest.kt b/kotlin-multiplatform/src/commonTest/kotlin/com/ricoh360/thetaclient/repository/options/OptionsTest.kt index c4f46a9538..0e85186481 100644 --- a/kotlin-multiplatform/src/commonTest/kotlin/com/ricoh360/thetaclient/repository/options/OptionsTest.kt +++ b/kotlin-multiplatform/src/commonTest/kotlin/com/ricoh360/thetaclient/repository/options/OptionsTest.kt @@ -16,6 +16,7 @@ import com.ricoh360.thetaclient.transferred.BurstOption import com.ricoh360.thetaclient.transferred.BurstOrder import com.ricoh360.thetaclient.transferred.CameraControlSource import com.ricoh360.thetaclient.transferred.CameraMode +import com.ricoh360.thetaclient.transferred.CameraPower import com.ricoh360.thetaclient.transferred.CaptureMode import com.ricoh360.thetaclient.transferred.EthernetConfig import com.ricoh360.thetaclient.transferred.FaceDetect @@ -95,6 +96,7 @@ class OptionsTest { ) val cameraControlSource = ThetaRepository.CameraControlSourceEnum.CAMERA val cameraMode = ThetaRepository.CameraModeEnum.CAPTURE + val cameraPower = ThetaRepository.CameraPowerEnum.ON val captureInterval = 6 val captureMode = ThetaRepository.CaptureModeEnum.IMAGE val captureNumber = 0 @@ -166,6 +168,7 @@ class OptionsTest { burstOption = burstOption, cameraControlSource = cameraControlSource, cameraMode = cameraMode, + cameraPower = cameraPower, captureInterval = captureInterval, captureMode = captureMode, captureNumber = captureNumber, @@ -231,6 +234,7 @@ class OptionsTest { assertEquals(options.getValue(ThetaRepository.OptionNameEnum.BurstOption), burstOption, "burstOption") assertEquals(options.getValue(ThetaRepository.OptionNameEnum.CameraControlSource), cameraControlSource, "cameraControlSource") assertEquals(options.getValue(ThetaRepository.OptionNameEnum.CameraMode), cameraMode, "cameraMode") + assertEquals(options.getValue(ThetaRepository.OptionNameEnum.CameraPower), cameraPower, "cameraPower") assertEquals(options.getValue(ThetaRepository.OptionNameEnum.CaptureInterval), captureInterval, "captureInterval") assertEquals(options.getValue(ThetaRepository.OptionNameEnum.CaptureMode), captureMode, "captureMode") assertEquals(options.getValue(ThetaRepository.OptionNameEnum.CaptureNumber), captureNumber, "captureNumber") @@ -324,6 +328,7 @@ class OptionsTest { ), Pair(ThetaRepository.OptionNameEnum.CameraControlSource, ThetaRepository.CameraControlSourceEnum.CAMERA), Pair(ThetaRepository.OptionNameEnum.CameraMode, ThetaRepository.CameraModeEnum.CAPTURE), + Pair(ThetaRepository.OptionNameEnum.CameraPower, ThetaRepository.CameraPowerEnum.ON), Pair(ThetaRepository.OptionNameEnum.CaptureInterval, 4), Pair(ThetaRepository.OptionNameEnum.CaptureMode, ThetaRepository.CaptureModeEnum.IMAGE), Pair(ThetaRepository.OptionNameEnum.CaptureNumber, 0), @@ -455,6 +460,7 @@ class OptionsTest { ) val cameraControlSource = Pair(CameraControlSource.CAMERA, ThetaRepository.CameraControlSourceEnum.CAMERA) val cameraMode = Pair(CameraMode.CAPTURE, ThetaRepository.CameraModeEnum.CAPTURE) + val cameraPower = Pair(CameraPower.OFF, ThetaRepository.CameraPowerEnum.OFF) val captureInterval = Pair(5, 5) val captureMode = Pair(CaptureMode.IMAGE, ThetaRepository.CaptureModeEnum.IMAGE) val captureNumber = Pair(9999, 9999) @@ -535,6 +541,7 @@ class OptionsTest { _burstOption = burstOption.first, _cameraControlSource = cameraControlSource.first, _cameraMode = cameraMode.first, + _cameraPower = cameraPower.first, captureInterval = captureInterval.first, captureMode = captureMode.first, captureNumber = captureNumber.first, @@ -595,6 +602,7 @@ class OptionsTest { assertEquals(options.burstOption, burstOption.second, "burstOption") assertEquals(options.cameraControlSource, cameraControlSource.second, "cameraControlSource") assertEquals(options.cameraMode, cameraMode.second, "cameraMode") + assertEquals(options.cameraPower, cameraPower.second, "cameraPower") assertEquals(options.captureInterval, captureInterval.second, "captureInterval") assertEquals(options.captureMode, captureMode.second, "captureMode") assertEquals(options.captureNumber, captureNumber.second, "captureNumber") @@ -701,6 +709,7 @@ class OptionsTest { ) val cameraControlSource = Pair(CameraControlSource.CAMERA, ThetaRepository.CameraControlSourceEnum.CAMERA) val cameraMode = Pair(CameraMode.CAPTURE, ThetaRepository.CameraModeEnum.CAPTURE) + val cameraPower = Pair(CameraPower.SILENT_MODE, ThetaRepository.CameraPowerEnum.SILENT_MODE) val captureInterval = Pair(20, 20) val captureMode = Pair(CaptureMode.IMAGE, ThetaRepository.CaptureModeEnum.IMAGE) val captureNumber = Pair(30, 30) @@ -784,6 +793,7 @@ class OptionsTest { burstOption = burstOption.second, cameraControlSource = cameraControlSource.second, cameraMode = cameraMode.second, + cameraPower = cameraPower.second, captureInterval = captureInterval.second, captureMode = captureMode.second, captureNumber = captureNumber.second, @@ -844,6 +854,7 @@ class OptionsTest { assertEquals(options._burstOption, burstOption.first, "burstOption") assertEquals(options._cameraControlSource, cameraControlSource.first, "cameraControlSource") assertEquals(options._cameraMode, cameraMode.first, "cameraMode") + assertEquals(options._cameraPower, cameraPower.first, "cameraPower") assertEquals(options.captureInterval, captureInterval.first, "captureInterval") assertEquals(options.captureMode, captureMode.first, "captureMode") assertEquals(options.captureNumber, captureNumber.first, "captureNumber") diff --git a/kotlin-multiplatform/src/commonTest/resources/VideoCapture/start_capture_done.json b/kotlin-multiplatform/src/commonTest/resources/VideoCapture/start_capture_done.json index 6537b10f4f..36ae3ac8a3 100644 --- a/kotlin-multiplatform/src/commonTest/resources/VideoCapture/start_capture_done.json +++ b/kotlin-multiplatform/src/commonTest/resources/VideoCapture/start_capture_done.json @@ -1 +1,8 @@ -{"name":"camera.startCapture","state":"done"} +{ + "_fileUrls": [ + "http://192.168.1.1/files/100RICOH/R0010429.MP4" + ], + "id": "154", + "name": "camera.startCapture", + "state": "done" +} diff --git a/kotlin-multiplatform/src/commonTest/resources/options/option_camera_power_saving.json b/kotlin-multiplatform/src/commonTest/resources/options/option_camera_power_saving.json new file mode 100644 index 0000000000..a7046ec301 --- /dev/null +++ b/kotlin-multiplatform/src/commonTest/resources/options/option_camera_power_saving.json @@ -0,0 +1 @@ +{"results":{"options":{"_cameraPower":"powerSaving"}},"name":"camera.getOptions","state":"done"} \ No newline at end of file diff --git a/kotlin-multiplatform/src/commonTest/resources/options/option_camera_unknown.json b/kotlin-multiplatform/src/commonTest/resources/options/option_camera_unknown.json new file mode 100644 index 0000000000..81576639d5 --- /dev/null +++ b/kotlin-multiplatform/src/commonTest/resources/options/option_camera_unknown.json @@ -0,0 +1 @@ +{"results":{"options":{"_cameraPower":"other"}},"name":"camera.getOptions","state":"done"} \ No newline at end of file diff --git a/react-native/android/build.gradle b/react-native/android/build.gradle index 6d2330eb92..5464a97d7c 100644 --- a/react-native/android/build.gradle +++ b/react-native/android/build.gradle @@ -135,7 +135,7 @@ dependencies { implementation "com.facebook.react:react-native:+" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3" - implementation "com.ricoh360.thetaclient:theta-client:1.10.2" + implementation "com.ricoh360.thetaclient:theta-client:1.11.0" // From node_modules } diff --git a/react-native/android/src/main/java/com/ricoh360/thetaclientreactnative/Converter.kt b/react-native/android/src/main/java/com/ricoh360/thetaclientreactnative/Converter.kt index 0d2c74febc..aecc356f3c 100644 --- a/react-native/android/src/main/java/com/ricoh360/thetaclientreactnative/Converter.kt +++ b/react-native/android/src/main/java/com/ricoh360/thetaclientreactnative/Converter.kt @@ -17,6 +17,7 @@ const val KEY_NOTIFY_PARAM_COMPLETION = "completion" const val KEY_NOTIFY_PARAM_EVENT = "event" const val KEY_NOTIFY_PARAM_MESSAGE = "message" const val KEY_NOTIFY_PARAM_STATUS = "status" +const val KEY_NOTIFY_PARAM_FILE_URL = "fileUrl" const val KEY_GPS_INFO = "gpsInfo" const val KEY_STATE_EXTERNAL_GPS_INFO = "externalGpsInfo" const val KEY_STATE_INTERNAL_GPS_INFO = "internalGpsInfo" @@ -34,6 +35,7 @@ val optionItemNameToEnum: Map = mutableMapOf( "burstOption" to OptionNameEnum.BurstOption, "cameraControlSource" to OptionNameEnum.CameraControlSource, "cameraMode" to OptionNameEnum.CameraMode, + "cameraPower" to OptionNameEnum.CameraPower, "captureInterval" to OptionNameEnum.CaptureInterval, "captureMode" to OptionNameEnum.CaptureMode, "captureNumber" to OptionNameEnum.CaptureNumber, @@ -121,6 +123,12 @@ fun toCapturingNotifyParam(status: CapturingStatusEnum): WritableMap { return result } +fun toStartedNotifyParam(value: String): WritableMap { + val result = Arguments.createMap() + result.putString(KEY_NOTIFY_PARAM_FILE_URL, value) + return result +} + fun toGpsInfo(map: ReadableMap): GpsInfo { return GpsInfo( latitude = map.getDouble("latitude").toFloat(), @@ -793,6 +801,7 @@ fun getOptionValueEnum(name: OptionNameEnum, valueName: String): Any? { OptionNameEnum.BurstMode -> BurstModeEnum.values().find { it.name == valueName } OptionNameEnum.CameraControlSource -> CameraControlSourceEnum.values().find { it.name == valueName } OptionNameEnum.CameraMode -> CameraModeEnum.values().find { it.name == valueName } + OptionNameEnum.CameraPower -> CameraPowerEnum.values().find { it.name == valueName } OptionNameEnum.CaptureMode -> CaptureModeEnum.values().find { it.name == valueName } OptionNameEnum.ContinuousNumber -> ContinuousNumberEnum.values().find { it.name == valueName } OptionNameEnum.ExposureCompensation -> ExposureCompensationEnum.values().find { it.name == valueName } diff --git a/react-native/android/src/main/java/com/ricoh360/thetaclientreactnative/ThetaClientSdkModule.kt b/react-native/android/src/main/java/com/ricoh360/thetaclientreactnative/ThetaClientSdkModule.kt index fa608a4275..10d7e2de26 100644 --- a/react-native/android/src/main/java/com/ricoh360/thetaclientreactnative/ThetaClientSdkModule.kt +++ b/react-native/android/src/main/java/com/ricoh360/thetaclientreactnative/ThetaClientSdkModule.kt @@ -23,6 +23,7 @@ class ThetaClientReactNativeModule( } var previewing: Boolean = false + var stopLivePreviewPromise: Promise? = null var photoCaptureBuilder: PhotoCapture.Builder? = null var photoCapture: PhotoCapture? = null var timeShiftCaptureBuilder: TimeShiftCapture.Builder? = null @@ -53,6 +54,7 @@ class ThetaClientReactNativeModule( var listenerCount: Int = 0 val messageNotInit: String = "Not initialized." + val messageLivePreviewRunning: String = "Live preview is running." /** * add event listener for [eventName] @@ -97,6 +99,7 @@ class ThetaClientReactNativeModule( try { theta = null previewing = false + stopLivePreviewPromise = null photoCaptureBuilder = null photoCapture = null timeShiftCaptureBuilder = null @@ -427,6 +430,11 @@ class ThetaClientReactNativeModule( */ @ReactMethod fun getLivePreview(promise: Promise) { + if (previewing) { + promise.reject(Exception(messageLivePreviewRunning)) + return + } + val theta = theta if (theta == null) { promise.reject(Exception(messageNotInit)) @@ -435,7 +443,7 @@ class ThetaClientReactNativeModule( fun ByteArray.toBase64(): String = String(Base64.getEncoder().encode(this)) suspend fun callFrameHandler(packet: Pair): Boolean { if (listenerCount == 0) { - return previewing + return stopLivePreviewPromise == null } val param = Arguments.createMap() param.putString( @@ -446,28 +454,49 @@ class ThetaClientReactNativeModule( reactApplicationContext .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) .emit(EVENT_NAME, param) - return previewing + return stopLivePreviewPromise == null } + previewing = true + execAndResetStopLivePreviewResolve(false) + launch { try { theta.getLivePreview(::callFrameHandler) + + execAndResetStopLivePreviewResolve(true) + promise.resolve(true) } catch (t: Throwable) { promise.reject(t) + } finally { + previewing = false } } } /** * stopLivePreview - stop live previewing + * @param promise promise to set result */ @ReactMethod - fun stopLivePreview() { + fun stopLivePreview(promise: Promise) { if (theta == null) { - throw Exception(messageNotInit) + promise.reject(Exception(messageNotInit)) + return + } + + if (!previewing || stopLivePreviewPromise != null) { + promise.resolve(false) + return } - previewing = false + + stopLivePreviewPromise = promise + } + + private fun execAndResetStopLivePreviewResolve(flag: Boolean) { + stopLivePreviewPromise?.resolve(flag) + stopLivePreviewPromise = null } /** @@ -749,6 +778,12 @@ class ThetaClientReactNativeModule( toNotify(NOTIFY_VIDEO_CAPTURE_CAPTURING, toCapturingNotifyParam(status = status)) ) } + + override fun onCaptureStarted(fileUrl: String?) { + sendNotifyEvent( + toNotify(NOTIFY_VIDEO_CAPTURE_STARTED, toStartedNotifyParam(fileUrl ?: "")) + ) + } } videoCapturing = videoCapture?.startCapture(StartCaptureCallback()) } @@ -2154,6 +2189,7 @@ class ThetaClientReactNativeModule( const val NOTIFY_TIMESHIFT_CAPTURING = "TIME-SHIFT-CAPTURING" const val NOTIFY_VIDEO_CAPTURE_STOP_ERROR = "VIDEO-CAPTURE-STOP-ERROR" const val NOTIFY_VIDEO_CAPTURE_CAPTURING = "VIDEO-CAPTURE-CAPTURING" + const val NOTIFY_VIDEO_CAPTURE_STARTED = "VIDEO-CAPTURE-STARTED" const val NOTIFY_LIMITLESS_INTERVAL_CAPTURE_STOP_ERROR = "LIMITLESS-INTERVAL-CAPTURE-STOP-ERROR" const val NOTIFY_LIMITLESS_INTERVAL_CAPTURE_CAPTURING = "LIMITLESS-INTERVAL-CAPTURE-CAPTURING" const val NOTIFY_SHOT_COUNT_SPECIFIED_INTERVAL_PROGRESS = "SHOT-COUNT-SPECIFIED-INTERVAL-PROGRESS" diff --git a/react-native/ios/ConvertUtil.swift b/react-native/ios/ConvertUtil.swift index 86b4e532ee..fcfb3c5510 100644 --- a/react-native/ios/ConvertUtil.swift +++ b/react-native/ios/ConvertUtil.swift @@ -9,6 +9,7 @@ let KEY_NOTIFY_PARAM_EVENT = "event" let KEY_NOTIFY_PARAM_IMAGE = "image" let KEY_NOTIFY_PARAM_MESSAGE = "message" let KEY_NOTIFY_PARAM_STATUS = "status" +let KEY_NOTIFY_PARAM_FILE_URL = "fileUrl" let KEY_DATETIME = "dateTime" let KEY_LANGUAGE = "language" let KEY_OFF_DELAY = "offDelay" @@ -88,6 +89,7 @@ let optionItemNameToEnum = [ "burstOption": ThetaRepository.OptionNameEnum.burstoption, "cameraControlSource": ThetaRepository.OptionNameEnum.cameracontrolsource, "cameraMode": ThetaRepository.OptionNameEnum.cameramode, + "cameraPower": ThetaRepository.OptionNameEnum.camerapower, "captureInterval": ThetaRepository.OptionNameEnum.captureinterval, "captureMode": ThetaRepository.OptionNameEnum.capturemode, "captureNumber": ThetaRepository.OptionNameEnum.capturenumber, @@ -210,6 +212,10 @@ func setOptionsValue(options: ThetaRepository.Options, name: String, value: Any) options.cameraMode = getEnumValue( values: ThetaRepository.CameraModeEnum.values(), name: value as! String )! + case ThetaRepository.OptionNameEnum.camerapower.name: + options.cameraPower = getEnumValue( + values: ThetaRepository.CameraPowerEnum.values(), name: value as! String + )! case ThetaRepository.OptionNameEnum.captureinterval.name: options.captureInterval = KotlinInt(integerLiteral: value as! Int) case ThetaRepository.OptionNameEnum.capturemode.name: @@ -464,6 +470,12 @@ func toCapturingNotifyParam(value: CapturingStatusEnum) -> [String: Any] { ] } +func toStartedNotifyParam(value: String) -> [String: Any] { + return [ + KEY_NOTIFY_PARAM_FILE_URL: value, + ] +} + // MARK: - Capture builder func setCaptureBuilderParams(params: [String: Any], builder: CaptureBuilder) { diff --git a/react-native/ios/ThetaClientReactNative.swift b/react-native/ios/ThetaClientReactNative.swift index 8c09c6769a..afc6c0010a 100644 --- a/react-native/ios/ThetaClientReactNative.swift +++ b/react-native/ios/ThetaClientReactNative.swift @@ -2,6 +2,7 @@ import THETAClient let ERROR_CODE_ERROR = "error" let MESSAGE_NOT_INIT = "Not initialized." +let MESSAGE_LIVE_PREVIEW_RUNNING = "Live Preview is running." let MESSAGE_NO_RESULT = "No result." let MESSAGE_NO_ARGUMENT = "No Argument." let MESSAGE_NO_PHOTO_CAPTURE = "No photoCapture." @@ -35,6 +36,7 @@ let MESSAGE_NO_EVENT_WEBSOCKET = "no eventWebSocket." class ThetaClientReactNative: RCTEventEmitter { var thetaRepository: ThetaRepository? var previewing = false + var stopLivePreviewResolve: RCTPromiseResolveBlock? var photoCaptureBuilder: PhotoCapture.Builder? var photoCapture: PhotoCapture? var timeShiftCaptureBuilder: TimeShiftCapture.Builder? @@ -74,6 +76,7 @@ class ThetaClientReactNative: RCTEventEmitter { static let NOTIFY_SHOT_COUNT_SPECIFIED_INTERVAL_CAPTURING = "SHOT-COUNT-SPECIFIED-INTERVAL-CAPTURING" static let NOTIFY_VIDEO_CAPTURE_STOP_ERROR = "VIDEO-CAPTURE-STOP-ERROR" static let NOTIFY_VIDEO_CAPTURE_CAPTURING = "VIDEO-CAPTURE-CAPTURING" + static let NOTIFY_VIDEO_CAPTURE_STARTED = "VIDEO-CAPTURE-STARTED" static let NOTIFY_LIMITLESS_INTERVAL_CAPTURE_STOP_ERROR = "LIMITLESS-INTERVAL-CAPTURE-STOP-ERROR" static let NOTIFY_LIMITLESS_INTERVAL_CAPTURE_CAPTURING = "LIMITLESS-INTERVAL-CAPTURE-CAPTURING" static let NOTIFY_COMPOSITE_INTERVAL_PROGRESS = "COMPOSITE-INTERVAL-PROGRESS" @@ -142,6 +145,7 @@ class ThetaClientReactNative: RCTEventEmitter { continuousCaptureBuilder = nil continuousCapture = nil previewing = false + stopLivePreviewResolve = nil eventWebSocket?.stop(completionHandler: { _ in }) eventWebSocket = nil @@ -450,6 +454,11 @@ class ThetaClientReactNative: RCTEventEmitter { resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock ) { + if previewing { + reject(ERROR_CODE_ERROR, MESSAGE_LIVE_PREVIEW_RUNNING, nil) + return + } + class FrameHandler: KotlinSuspendFunction1 { let thetaClientReactNative: ThetaClientReactNative static let FrameInterval = CFTimeInterval(1.0 / 10.0) @@ -476,7 +485,7 @@ class ThetaClientReactNative: RCTEventEmitter { } } } - return thetaClientReactNative.previewing + return thetaClientReactNative.stopLivePreviewResolve == nil } } @@ -485,11 +494,17 @@ class ThetaClientReactNative: RCTEventEmitter { return } let frameHandler = FrameHandler(self) + previewing = true + execAndResetStopLivePreviewResolve(false) + thetaRepository.getLivePreview(frameHandler: frameHandler) { error in + self.previewing = false if let error { reject(ERROR_CODE_ERROR, error.localizedDescription, error) } else { + self.execAndResetStopLivePreviewResolve(true) + resolve(true) } } @@ -497,11 +512,19 @@ class ThetaClientReactNative: RCTEventEmitter { @objc(stopLivePreview:withRejecter:) func stopLivePreview( - resolve: RCTPromiseResolveBlock, - reject _: RCTPromiseRejectBlock + resolve: @escaping RCTPromiseResolveBlock, + reject _: @escaping RCTPromiseRejectBlock ) { - previewing = false - resolve(nil) + if !previewing || stopLivePreviewResolve != nil { + resolve(false) + return + } + stopLivePreviewResolve = resolve + } + + private func execAndResetStopLivePreviewResolve(_ flag: Bool) { + stopLivePreviewResolve?(flag) + stopLivePreviewResolve = nil } @objc(getPhotoCaptureBuilder:withRejecter:) @@ -841,6 +864,16 @@ class ThetaClientReactNative: RCTEventEmitter { ) ) } + + func onCaptureStarted(fileUrl: String?) { + client?.sendEvent( + withName: ThetaClientReactNative.EVENT_NOTIFY, + body: toNotify( + name: ThetaClientReactNative.NOTIFY_VIDEO_CAPTURE_STARTED, + params: toStartedNotifyParam(value: fileUrl ?? "") + ) + ) + } } videoCapturing = videoCapture.startCapture( diff --git a/react-native/package.json b/react-native/package.json index 79f0c1983d..408d5b167a 100644 --- a/react-native/package.json +++ b/react-native/package.json @@ -1,6 +1,6 @@ { "name": "theta-client-react-native", - "version": "1.10.2", + "version": "1.11.0", "description": "This library provides a way to control RICOH THETA using.", "main": "lib/commonjs/index", "module": "lib/module/index", @@ -68,7 +68,7 @@ "pod-install": "^0.1.0", "prettier": "^2.8.1", "react": "18.2.0", - "react-native": "0.71.14", + "react-native": "0.71.19", "react-native-builder-bob": "^0.20.3", "release-it": "^15.5.1", "typescript": "^4.9.4" diff --git a/react-native/src/__tests__/capture/video-capture.test.ts b/react-native/src/__tests__/capture/video-capture.test.ts index aad8f5e1fb..bf5efe1938 100644 --- a/react-native/src/__tests__/capture/video-capture.test.ts +++ b/react-native/src/__tests__/capture/video-capture.test.ts @@ -266,4 +266,66 @@ describe('video capture', () => { return promise; }); + + test('onCaptureStarted', async () => { + let notifyCallback: (notify: BaseNotify) => void = () => { + expect(true).toBeFalsy(); + }; + jest.mocked(NativeEventEmitter_addListener).mockImplementation( + jest.fn((_, callback) => { + notifyCallback = callback; + return { + remove: jest.fn(), + }; + }) + ); + + await initialize(); + const builder = getVideoCaptureBuilder(); + jest + .mocked(thetaClient.buildVideoCapture) + .mockImplementation(jest.fn(async () => {})); + const testUrl = 'http://192.168.1.1/files/100RICOH/R100.MP4'; + + const sendFileUrl = (fileUrl: String) => { + notifyCallback({ + name: 'VIDEO-CAPTURE-STARTED', + params: { + fileUrl: fileUrl, + }, + }); + }; + + jest.mocked(thetaClient.startVideoCapture).mockImplementation( + jest.fn(async () => { + sendFileUrl('xxx.mp4'); + return testUrl; + }) + ); + + const capture = await builder.build(); + let isStarted = false; + const result = await capture.startCapture( + undefined, + undefined, + (fileUrl) => { + expect(fileUrl).toBe('xxx.mp4'); + isStarted = true; + } + ); + expect(result).toBe(testUrl); + + let done: (value: unknown) => void; + const promise = new Promise((resolve) => { + done = resolve; + }); + + setTimeout(() => { + expect(NotifyController.instance.notifyList.size).toBe(0); + expect(isStarted).toBeTruthy(); + done(0); + }, 1); + + return promise; + }); }); diff --git a/react-native/src/__tests__/options/option-camera-power.test.tsx b/react-native/src/__tests__/options/option-camera-power.test.tsx new file mode 100644 index 0000000000..3613d0f1a1 --- /dev/null +++ b/react-native/src/__tests__/options/option-camera-power.test.tsx @@ -0,0 +1,21 @@ +import { CameraPowerEnum } from '../../theta-repository/options/option-camera-power'; + +describe('CameraPowerEnum', () => { + const data: string[][] = [ + [CameraPowerEnum.UNKNOWN, 'UNKNOWN'], + [CameraPowerEnum.ON, 'ON'], + [CameraPowerEnum.OFF, 'OFF'], + [CameraPowerEnum.POWER_SAVING, 'POWER_SAVING'], + [CameraPowerEnum.SILENT_MODE, 'SILENT_MODE'], + ]; + + test('length', () => { + expect(data.length).toBe(Object.keys(CameraPowerEnum).length); + }); + + test('data', () => { + data.forEach((item) => { + expect(item[0] ? item[0].toString() : '').toBe(item[1]); + }); + }); +}); diff --git a/react-native/src/capture/video-capture.ts b/react-native/src/capture/video-capture.ts index f0f15e75ca..58e4f503e8 100644 --- a/react-native/src/capture/video-capture.ts +++ b/react-native/src/capture/video-capture.ts @@ -13,6 +13,7 @@ const ThetaClientReactNative = NativeModules.ThetaClientReactNative; const NOTIFY_NAME = 'VIDEO-CAPTURE-STOP-ERROR'; const NOTIFY_CAPTURING = 'VIDEO-CAPTURE-CAPTURING'; +const NOTIFY_STARTED = 'VIDEO-CAPTURE-STARTED'; interface CaptureStopErrorNotify extends BaseNotify { params?: { @@ -26,6 +27,12 @@ interface CapturingNotify extends BaseNotify { }; } +interface CaptureStartedNotify extends BaseNotify { + params?: { + fileUrl: string; + }; +} + /** * VideoCapture class */ @@ -39,11 +46,13 @@ export class VideoCapture { * start video capture * @param onStopFailed the block for error of stopCapture * @param onCapturing Called when change capture status + * @param onCaptureStarted Called when capture started * @return promise of captured file url */ startCapture( onStopFailed?: (error: any) => void, - onCapturing?: (status: CapturingStatusEnum) => void + onCapturing?: (status: CapturingStatusEnum) => void, + onCaptureStarted?: (fileUrl?: string) => void ): Promise { if (onStopFailed) { this.notify.addNotify(NOTIFY_NAME, (event: CaptureStopErrorNotify) => { @@ -57,6 +66,13 @@ export class VideoCapture { } }); } + if (onCaptureStarted) { + this.notify.addNotify(NOTIFY_STARTED, (event: CaptureStartedNotify) => { + onCaptureStarted( + event.params?.fileUrl !== '' ? event.params?.fileUrl : undefined + ); + }); + } return new Promise(async (resolve, reject) => { await ThetaClientReactNative.startVideoCapture() .then((result?: string) => { @@ -68,6 +84,7 @@ export class VideoCapture { .finally(() => { this.notify.removeNotify(NOTIFY_NAME); this.notify.removeNotify(NOTIFY_CAPTURING); + this.notify.removeNotify(NOTIFY_STARTED); }); }); } diff --git a/react-native/src/theta-repository/options/index.ts b/react-native/src/theta-repository/options/index.ts index 5154bd7f37..781aa3a04d 100644 --- a/react-native/src/theta-repository/options/index.ts +++ b/react-native/src/theta-repository/options/index.ts @@ -7,6 +7,7 @@ export * from './option-burst-mode'; export * from './option-burst-option'; export * from './option-camera-control-source'; export * from './option-camera-mode'; +export * from './option-camera-power'; export * from './option-capture-mode'; export * from './option-continuous-number'; export * from './option-face-detect'; diff --git a/react-native/src/theta-repository/options/option-camera-power.ts b/react-native/src/theta-repository/options/option-camera-power.ts new file mode 100644 index 0000000000..a69503129a --- /dev/null +++ b/react-native/src/theta-repository/options/option-camera-power.ts @@ -0,0 +1,28 @@ +/** + * _cameraPower is the power status of camera. + * + * For RICOH THETA X v2.61.0 or later + */ +export const CameraPowerEnum = { + /** Undefined value */ + UNKNOWN: 'UNKNOWN', + /** Power ON */ + ON: 'ON', + /** Power OFF */ + OFF: 'OFF', + /** + * Power on, power saving mode. Camera is closed. + * Unavailable parameter when plugin is running. In this case, invalidParameterValue error will be returned. + */ + POWER_SAVING: 'POWER_SAVING', + + /** + * Power on, silent mode. LCD/LED is turned off. + * Unavailable parameter when plugin is running. In this case, invalidParameterValue error will be returned. + */ + SILENT_MODE: 'SILENT_MODE', +} as const; + +/** type definition of CameraPowerEnum */ +export type CameraPowerEnum = + (typeof CameraPowerEnum)[keyof typeof CameraPowerEnum]; diff --git a/react-native/src/theta-repository/options/options.ts b/react-native/src/theta-repository/options/options.ts index 5aa9416198..470f5e458d 100644 --- a/react-native/src/theta-repository/options/options.ts +++ b/react-native/src/theta-repository/options/options.ts @@ -32,6 +32,7 @@ import type { OffDelayEnum } from './option-off-delay'; import type { SleepDelayEnum } from './option-sleep-delay'; import type { EthernetConfig } from './option-ethernet-config'; import type { FileFormatEnum } from './option-file-format'; +import type { CameraPowerEnum } from './option-camera-power'; /** Aperture value. */ export const ApertureEnum = { @@ -348,6 +349,8 @@ export const OptionNameEnum = { CameraControlSource: 'CameraControlSource', /** cameraMode */ CameraMode: 'CameraMode', + /** cameraPower */ + CameraPower: 'CameraPower', /** captureInterval */ CaptureInterval: 'CaptureInterval', /** captureMode */ @@ -474,6 +477,8 @@ export type Options = { cameraControlSource?: CameraControlSourceEnum; /** Camera mode. */ cameraMode?: CameraModeEnum; + /** Camera power state */ + cameraPower?: CameraPowerEnum; /** * Shooting interval (sec.) for interval shooting. * diff --git a/react-native/src/theta-repository/theta-repository.ts b/react-native/src/theta-repository/theta-repository.ts index 676e6fe5cf..ac39b6d51f 100644 --- a/react-native/src/theta-repository/theta-repository.ts +++ b/react-native/src/theta-repository/theta-repository.ts @@ -204,9 +204,10 @@ export function getLivePreview(): Promise { * Stop live preview. * * @function StopLivePreview + * @return promise of boolean result */ -export function stopLivePreview() { - ThetaClientReactNative.stopLivePreview(); +export function stopLivePreview(): Promise { + return ThetaClientReactNative.stopLivePreview(); } /** diff --git a/react-native/theta-client-react-native.podspec b/react-native/theta-client-react-native.podspec index b39ce33b11..21ed79af50 100644 --- a/react-native/theta-client-react-native.podspec +++ b/react-native/theta-client-react-native.podspec @@ -17,7 +17,7 @@ Pod::Spec.new do |s| s.source_files = "ios/**/*.{h,m,mm,swift}" s.dependency "React-Core" - s.dependency "THETAClient", "1.10.2" + s.dependency "THETAClient", "1.11.0" # Don't install the dependencies when we run `pod install` in the old architecture. if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then diff --git a/react-native/verification-tool/ios/PrivacyInfo.xcprivacy b/react-native/verification-tool/ios/PrivacyInfo.xcprivacy new file mode 100644 index 0000000000..30275f2907 --- /dev/null +++ b/react-native/verification-tool/ios/PrivacyInfo.xcprivacy @@ -0,0 +1,39 @@ + + + + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryFileTimestamp + NSPrivacyAccessedAPITypeReasons + + C617.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryUserDefaults + NSPrivacyAccessedAPITypeReasons + + CA92.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategorySystemBootTime + NSPrivacyAccessedAPITypeReasons + + 35F9.1 + + + + NSPrivacyCollectedDataTypes + + NSPrivacyTrackingDomains + + NSPrivacyTracking + + + diff --git a/react-native/verification-tool/ios/ThetaClientVerificationTool.xcodeproj/project.pbxproj b/react-native/verification-tool/ios/ThetaClientVerificationTool.xcodeproj/project.pbxproj index 8f85abcfa3..0776f5d9ba 100644 --- a/react-native/verification-tool/ios/ThetaClientVerificationTool.xcodeproj/project.pbxproj +++ b/react-native/verification-tool/ios/ThetaClientVerificationTool.xcodeproj/project.pbxproj @@ -14,6 +14,7 @@ 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; 7699B88040F8A987B510C191 /* libPods-ThetaClientVerificationTool-ThetaClientVerificationToolTest.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 19F6CBCC0A4E27FBF8BF4A61 /* libPods-ThetaClientVerificationTool-ThetaClientVerificationToolTest.a */; }; 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; + 961CBFE12CA51F9B00BF87D5 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 961CBFE02CA51F9B00BF87D5 /* PrivacyInfo.xcprivacy */; }; C9397F842AB2E5E200F7CC61 /* Web.bundle in Resources */ = {isa = PBXBuildFile; fileRef = C9397F832AB2E5E200F7CC61 /* Web.bundle */; }; /* End PBXBuildFile section */ @@ -44,6 +45,7 @@ 5DCACB8F33CDC322A6C60F78 /* libPods-ThetaClientVerificationTool.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ThetaClientVerificationTool.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = ThetaClientVerificationTool/LaunchScreen.storyboard; sourceTree = ""; }; 89C6BE57DB24E9ADA2F236DE /* Pods-ThetaClientVerificationTool-ThetaClientVerificationToolTest.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ThetaClientVerificationTool-ThetaClientVerificationToolTest.release.xcconfig"; path = "Target Support Files/Pods-ThetaClientVerificationTool-ThetaClientVerificationToolTest/Pods-ThetaClientVerificationTool-ThetaClientVerificationToolTest.release.xcconfig"; sourceTree = ""; }; + 961CBFE02CA51F9B00BF87D5 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; C9397F832AB2E5E200F7CC61 /* Web.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; name = Web.bundle; path = ../web/Web.bundle; sourceTree = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; /* End PBXFileReference section */ @@ -88,6 +90,7 @@ 13B07FAE1A68108700A75B9A /* ThetaClientVerificationTool */ = { isa = PBXGroup; children = ( + 961CBFE02CA51F9B00BF87D5 /* PrivacyInfo.xcprivacy */, 13B07FAF1A68108700A75B9A /* AppDelegate.h */, 13B07FB01A68108700A75B9A /* AppDelegate.mm */, 13B07FB51A68108700A75B9A /* Images.xcassets */, @@ -247,6 +250,7 @@ files = ( C9397F842AB2E5E200F7CC61 /* Web.bundle in Resources */, 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */, + 961CBFE12CA51F9B00BF87D5 /* PrivacyInfo.xcprivacy in Resources */, 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -580,6 +584,7 @@ GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", + _LIBCPP_ENABLE_CXX17_REMOVED_UNARY_BINARY_FUNCTION, ); GCC_SYMBOLS_PRIVATE_EXTERN = NO; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -606,6 +611,7 @@ "-DFOLLY_MOBILE=1", "-DFOLLY_USE_LIBCPP=1", ); + OTHER_LDFLAGS = "$(inherited)"; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; }; @@ -647,6 +653,10 @@ "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + _LIBCPP_ENABLE_CXX17_REMOVED_UNARY_BINARY_FUNCTION, + ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; @@ -670,6 +680,7 @@ "-DFOLLY_MOBILE=1", "-DFOLLY_USE_LIBCPP=1", ); + OTHER_LDFLAGS = "$(inherited)"; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; diff --git a/react-native/verification-tool/package.json b/react-native/verification-tool/package.json index 6eebb0ffc4..022a79aa1f 100644 --- a/react-native/verification-tool/package.json +++ b/react-native/verification-tool/package.json @@ -14,7 +14,7 @@ "@react-navigation/native-stack": "^6.9.13", "marzipano": "0.10.2", "react": "18.2.0", - "react-native": "0.71.14", + "react-native": "0.71.19", "react-native-safe-area-context": "^4.7.1", "react-native-screens": "^3.24.0", "react-native-webview": "^13.6.0" diff --git a/react-native/verification-tool/src/screen/live-preview-screen/live-preview-screen.tsx b/react-native/verification-tool/src/screen/live-preview-screen/live-preview-screen.tsx index b030ec94ed..dbb77617f8 100644 --- a/react-native/verification-tool/src/screen/live-preview-screen/live-preview-screen.tsx +++ b/react-native/verification-tool/src/screen/live-preview-screen/live-preview-screen.tsx @@ -35,17 +35,17 @@ const LivePreviewScreen: React.FC< const startLivePreview = async () => { setPreviewing(true); - getLivePreview() - .then((x) => { - setPreviewing(false); - console.log(`live preview done with ${x}`); - }) - .catch((error) => { - setPreviewing(false); - Alert.alert('getLivePreview', 'error: \n' + JSON.stringify(error), [ - { text: 'OK' }, - ]); - }); + try { + const ret = await getLivePreview(); + setPreviewing(false); + console.log(`live preview done with ${ret}`); + } catch (error) { + Alert.alert('getLivePreview', 'error: \n' + JSON.stringify(error), [ + { text: 'OK' }, + ]); + } finally { + setPreviewing(false); + } }; const setFrameData = (data: string) => { @@ -63,7 +63,9 @@ const LivePreviewScreen: React.FC< const onStop = () => { isInitialized().then((isInit) => { if (isInit) { - stopLivePreview(); + stopLivePreview().then((isStopped) => { + console.log(`isStop = ${isStopped}`); + }); } else { Alert.alert('stopLivePreview', 'error: Not initialized.', [ { text: 'OK' }, @@ -116,7 +118,9 @@ const LivePreviewScreen: React.FC< /> - {previewing ? 'Previewing...' : 'Stopped'} + + {previewing ? 'Previewing...' : 'Stopped'} +