diff --git a/.github/workflows/buildAndTest.yaml b/.github/workflows/buildAndTest.yaml index c91e02d6c4..a2a63a68b3 100644 --- a/.github/workflows/buildAndTest.yaml +++ b/.github/workflows/buildAndTest.yaml @@ -14,10 +14,10 @@ jobs: runs-on: macos-12 steps: - uses: actions/checkout@v3 - - name: Set up JDK 11 + - name: Set up JDK 17 uses: actions/setup-java@v3 with: - java-version: '11' + java-version: '17' distribution: 'temurin' - name: Build and Test with Gradle uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1 diff --git a/.github/workflows/kotlin_checker.yml b/.github/workflows/kotlin_checker.yml index ce53db750d..afd8182813 100644 --- a/.github/workflows/kotlin_checker.yml +++ b/.github/workflows/kotlin_checker.yml @@ -10,6 +10,11 @@ jobs: with: ruby-version: "2.7" architecture: "x64" + - name: Setup JDK + uses: actions/setup-java@v3 + with: + java-version: "17" + distribution: "temurin" - name: install danger run: | gem install bundler diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index c8f1db697f..67248f205f 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -18,10 +18,10 @@ jobs: with: path: gh-pages ref: gh-pages - - name: Set up JDK 11 + - name: Set up JDK 17 uses: actions/setup-java@v3 with: - java-version: "11" + java-version: "17" distribution: "temurin" - name: Setup for build run: | diff --git a/README.md b/README.md index cae47e7d3e..2e5b4745ec 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,14 @@ # THETA Client +[![theta-client CI with Gradle](https://github.com/ricohapi/theta-client/actions/workflows/buildAndTest.yaml/badge.svg)](https://github.com/ricohapi/theta-client/actions/workflows/buildAndTest.yaml) +[![KDoc](https://img.shields.io/badge/API_reference-KDoc-green.svg)](https://ricohapi.github.io/theta-client/) + +[![Kotlin](https://img.shields.io/badge/Kotlin-1.9.10-navy.svg?style=flat&logo=kotlin)](https://kotlinlang.org) +[![Swift](https://img.shields.io/badge/for_Swift-5-FF9900.svg)](https://kotlinlang.org) +[![Flutter](https://img.shields.io/badge/for_Flutter->=2.5.0-blue.svg)](https://ricohapi.github.io/theta-client/) +[![React Native](https://img.shields.io/badge/for_React_Native-0.70.8-aqua.svg)](https://ricohapi.github.io/theta-client/) + + This library provides a way to control RICOH THETA using [RICOH THETA API v2.1](https://github.com/ricohapi/theta-api-specs/tree/main/theta-web-api-v2.1). Your app can perform the following actions: * Take a photo and video @@ -54,8 +63,16 @@ theta-client$ ./gradlew testReleaseUnitTest ``` ## How to Use -See tutorials in `docs` directory and [KDoc](https://ricohapi.github.io/theta-client/) of this library. +See tutorials in `docs` directory and [API reference](https://ricohapi.github.io/theta-client/) of this library. +Demo applications in `demos` directory may help you understand how to use this library. + +## Verification tool +A tool, written in React Native, is prepared to verify the responses and behavior of Theta to verious requests. +Using this verification tool, you can select and send a command with its parameters then its response is displayed in JSON converted from Android or iOS object. +![menu screen](docs/assets/screen_menu.jpg) +![info screen](docs/assets/screen_info.jpg) +![capture screen](docs/assets/screen_capture.jpg) ## License diff --git a/build.gradle.kts b/build.gradle.kts index 94f37db822..0b77444c9f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,24 +1,16 @@ -buildscript { - repositories { - gradlePluginPortal() - google() - mavenCentral() - } - dependencies { - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.20") - classpath("com.android.tools.build:gradle:7.2.2") - classpath("org.jetbrains.dokka:dokka-gradle-plugin:1.8.20") - classpath("org.jetbrains.dokka:versioning-plugin:1.8.20") - } +plugins { + //trick: for the same plugin versions in all sub-modules + id("com.android.library").version("8.1.2").apply(false) + kotlin("multiplatform").version("1.9.10").apply(false) + kotlin("plugin.serialization").version("1.9.10").apply(false) + id("org.jetbrains.dokka").version("1.9.10") } -allprojects { - repositories { - google() - mavenCentral() +buildscript { + dependencies { + classpath("org.jetbrains.dokka:versioning-plugin:1.9.10") } } tasks.register("clean", Delete::class) { - delete(rootProject.buildDir) } diff --git a/demos/demo-android/app/build.gradle b/demos/demo-android/app/build.gradle index ec8ceb2156..fb56f7147e 100755 --- a/demos/demo-android/app/build.gradle +++ b/demos/demo-android/app/build.gradle @@ -5,12 +5,12 @@ plugins { } android { - compileSdk 33 + compileSdk 34 defaultConfig { applicationId "com.ricoh360.thetaclient.thetaClientDemo" minSdk 26 - targetSdk 33 + targetSdk 34 versionCode 1 versionName "1.0.0" @@ -36,6 +36,7 @@ android { } buildFeatures { compose true + buildConfig true } composeOptions { kotlinCompilerExtensionVersion compose_version @@ -57,18 +58,18 @@ android { dependencies { - implementation 'androidx.core:core-ktx:1.9.0' + implementation 'androidx.core:core-ktx:1.12.0' implementation "androidx.compose.ui:ui:$compose_version" implementation "androidx.compose.material:material:$compose_version" implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" - implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1' - implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1' - implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1' - implementation 'androidx.activity:activity-compose:1.6.1' - implementation "androidx.navigation:navigation-compose:2.5.3" - implementation 'androidx.webkit:webkit:1.5.0' + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2' + implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2' + implementation 'androidx.activity:activity-compose:1.8.0' + implementation "androidx.navigation:navigation-compose:2.7.5" + implementation 'androidx.webkit:webkit:1.8.0' implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" - implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.3' + implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0' implementation 'com.jakewharton.timber:timber:5.0.1' implementation 'io.coil-kt:coil-compose:2.2.2' implementation "io.ktor:ktor-client-cio:2.1.3" @@ -77,8 +78,8 @@ dependencies { testImplementation 'org.junit.jupiter:junit-jupiter:5.9.0' testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version" testImplementation 'org.junit.jupiter:junit-jupiter' - androidTestImplementation 'androidx.test.ext:junit:1.1.4' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0' + androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version" diff --git a/demos/demo-android/build.gradle b/demos/demo-android/build.gradle index 6e06fcab11..ed414d608c 100755 --- a/demos/demo-android/build.gradle +++ b/demos/demo-android/build.gradle @@ -1,7 +1,7 @@ buildscript { ext { - compose_version = '1.3.1' // depends on Kotlin 1.7.10 - kotlin_version = '1.7.10' + compose_version = '1.5.3' // depends on Kotlin 1.7.10 + kotlin_version = '1.9.10' coroutines_version = '1.6.4' } dependencies { @@ -12,8 +12,8 @@ buildscript { // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id 'com.android.application' version '7.3.1' apply false - id 'com.android.library' version '7.3.1' apply false + id 'com.android.application' version '8.1.2' apply false + id 'com.android.library' version '8.1.2' apply false id 'org.jetbrains.kotlin.android' version "$kotlin_version" apply false } diff --git a/demos/demo-android/gradle/wrapper/gradle-wrapper.properties b/demos/demo-android/gradle/wrapper/gradle-wrapper.properties index b6fb354253..4f749d8449 100755 --- a/demos/demo-android/gradle/wrapper/gradle-wrapper.properties +++ b/demos/demo-android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Tue Jul 05 14:22:13 JST 2022 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/demos/demo-react-native/.eslintrc.js b/demos/demo-react-native/.eslintrc.js index dcf0be0865..1f10b3ae48 100644 --- a/demos/demo-react-native/.eslintrc.js +++ b/demos/demo-react-native/.eslintrc.js @@ -13,4 +13,5 @@ module.exports = { }, }, ], + ignorePatterns: ['marzipano.js'], }; diff --git a/demos/demo-react-native/.gitignore b/demos/demo-react-native/.gitignore index 2423126f72..0efef7b48e 100644 --- a/demos/demo-react-native/.gitignore +++ b/demos/demo-react-native/.gitignore @@ -62,3 +62,6 @@ buck-out/ # Ruby / CocoaPods /ios/Pods/ /vendor/bundle/ + +# live preview +marzipano.js diff --git a/demos/demo-react-native/android/app/build.gradle b/demos/demo-react-native/android/app/build.gradle index 2697cc1a8f..9c95436efc 100644 --- a/demos/demo-react-native/android/app/build.gradle +++ b/demos/demo-react-native/android/app/build.gradle @@ -1,7 +1,7 @@ apply plugin: "com.android.application" +apply plugin: "com.facebook.react" import com.android.build.OutputFile -import org.apache.tools.ant.taskdefs.condition.Os /** * The react.gradle file registers a task for each build variant (e.g. bundleDebugJsAndAssets @@ -155,6 +155,7 @@ android { } if (!enableSeparateBuildPerCPUArchitecture) { ndk { + //noinspection ChromeOsAbiSupport abiFilters (*reactNativeArchitectures()) } } @@ -207,6 +208,7 @@ android { reset() enable enableSeparateBuildPerCPUArchitecture universalApk false // If true, also generate a universal APK + //noinspection ChromeOsAbiSupport include (*reactNativeArchitectures()) } } @@ -246,15 +248,21 @@ android { } } + + sourceSets { + main { + assets.srcDirs = ["../../web/Web.bundle"] + } + } } dependencies { implementation fileTree(dir: "libs", include: ["*.jar"]) - //noinspection GradleDynamicVersion - implementation "com.facebook.react:react-native:+" // From node_modules + // The version of react-native is set by the React Native Gradle Plugin + implementation("com.facebook.react:react-android") - implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0" + implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") { exclude group:'com.facebook.fbjni' @@ -270,10 +278,7 @@ dependencies { } if (enableHermes) { - //noinspection GradleDynamicVersion - implementation("com.facebook.react:hermes-engine:+") { // From node_modules - exclude group:'com.facebook.fbjni' - } + implementation("com.facebook.react:hermes-android") } else { implementation jscFlavor } @@ -283,7 +288,7 @@ if (isNewArchitectureEnabled()) { // If new architecture is enabled, we let you build RN from source // Otherwise we fallback to a prebuilt .aar bundled in the NPM package. // This will be applied to all the imported transtitive dependency. - configurations.all { + configurations.configureEach { resolutionStrategy.dependencySubstitution { substitute(module("com.facebook.react:react-native")) .using(project(":ReactAndroid")) @@ -297,7 +302,7 @@ if (isNewArchitectureEnabled()) { // Run this once to be able to run the application with BUCK // puts all compile dependencies into folder libs for BUCK to use -task copyDownloadableDepsToLibs(type: Copy) { +tasks.register('copyDownloadableDepsToLibs', Copy) { from configurations.implementation into 'libs' } diff --git a/demos/demo-react-native/android/build.gradle b/demos/demo-react-native/android/build.gradle index dc51df98f5..1809e1023e 100644 --- a/demos/demo-react-native/android/build.gradle +++ b/demos/demo-react-native/android/build.gradle @@ -2,25 +2,22 @@ buildscript { ext { - buildToolsVersion = "31.0.0" + buildToolsVersion = "33.0.0" minSdkVersion = 26 - compileSdkVersion = 31 - targetSdkVersion = 31 + compileSdkVersion = 33 + targetSdkVersion = 33 - if (System.properties['os.arch'] == "aarch64") { - // For M1 Users we need to use the NDK 24 which added support for aarch64 - ndkVersion = "24.0.8215888" - } else { - // Otherwise we default to the side-by-side NDK version from AGP. - ndkVersion = "21.4.7075529" - } + // We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP. + ndkVersion = "23.1.7779620" + + ext.kotlinVersion = "1.9.10" } repositories { google() mavenCentral() } dependencies { - classpath("com.android.tools.build:gradle:7.2.1") + classpath("com.android.tools.build:gradle:7.3.1") classpath("com.facebook.react:react-native-gradle-plugin") classpath("de.undercouch:gradle-download-task:5.0.1") // NOTE: Do not place your application dependencies here; they belong diff --git a/demos/demo-react-native/ios/DemoReactNative.xcodeproj/project.pbxproj b/demos/demo-react-native/ios/DemoReactNative.xcodeproj/project.pbxproj index 513dafd721..988bbe1a47 100644 --- a/demos/demo-react-native/ios/DemoReactNative.xcodeproj/project.pbxproj +++ b/demos/demo-react-native/ios/DemoReactNative.xcodeproj/project.pbxproj @@ -14,6 +14,7 @@ 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; 7699B88040F8A987B510C191 /* libPods-DemoReactNative-DemoReactNativeTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 19F6CBCC0A4E27FBF8BF4A61 /* libPods-DemoReactNative-DemoReactNativeTests.a */; }; 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; + C92A28D12AF0D16800EFE8E2 /* Web.bundle in Resources */ = {isa = PBXBuildFile; fileRef = C92A28D02AF0D16800EFE8E2 /* Web.bundle */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -43,6 +44,7 @@ 5DCACB8F33CDC322A6C60F78 /* libPods-DemoReactNative.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-DemoReactNative.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = DemoReactNative/LaunchScreen.storyboard; sourceTree = ""; }; 89C6BE57DB24E9ADA2F236DE /* Pods-DemoReactNative-DemoReactNativeTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DemoReactNative-DemoReactNativeTests.release.xcconfig"; path = "Target Support Files/Pods-DemoReactNative-DemoReactNativeTests/Pods-DemoReactNative-DemoReactNativeTests.release.xcconfig"; sourceTree = ""; }; + C92A28D02AF0D16800EFE8E2 /* 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 */ @@ -92,6 +94,7 @@ 13B07FB61A68108700A75B9A /* Info.plist */, 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */, 13B07FB71A68108700A75B9A /* main.m */, + C92A28D02AF0D16800EFE8E2 /* Web.bundle */, ); name = DemoReactNative; sourceTree = ""; @@ -242,6 +245,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + C92A28D12AF0D16800EFE8E2 /* Web.bundle in Resources */, 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */, 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, ); diff --git a/demos/demo-react-native/ios/DemoReactNative/AppDelegate.mm b/demos/demo-react-native/ios/DemoReactNative/AppDelegate.mm index fd851239f7..05abcae55a 100644 --- a/demos/demo-react-native/ios/DemoReactNative/AppDelegate.mm +++ b/demos/demo-react-native/ios/DemoReactNative/AppDelegate.mm @@ -31,7 +31,7 @@ @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - RCTAppSetupPrepareApp(application); + RCTAppSetupPrepareApp(application, true); RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions]; @@ -44,7 +44,7 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( #endif NSDictionary *initProps = [self prepareInitialProps]; - UIView *rootView = RCTAppSetupDefaultRootView(bridge, @"DemoReactNative", initProps); + UIView *rootView = RCTAppSetupDefaultRootView(bridge, @"DemoReactNative", initProps, true); if (@available(iOS 13.0, *)) { rootView.backgroundColor = [UIColor systemBackgroundColor]; diff --git a/demos/demo-react-native/ios/Podfile.lock b/demos/demo-react-native/ios/Podfile.lock index aead8cc19e..62565f3286 100644 --- a/demos/demo-react-native/ios/Podfile.lock +++ b/demos/demo-react-native/ios/Podfile.lock @@ -304,6 +304,8 @@ PODS: - glog - react-native-safe-area-context (4.7.3): - React-Core + - react-native-webview (13.6.2): + - React-Core - React-perflogger (0.70.6) - React-RCTActionSheet (0.70.6): - React-Core/RCTActionSheetHeaders (= 0.70.6) @@ -430,6 +432,7 @@ DEPENDENCIES: - React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`) - React-logger (from `../node_modules/react-native/ReactCommon/logger`) - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) + - react-native-webview (from `../node_modules/react-native-webview`) - React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`) - React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`) - React-RCTAnimation (from `../node_modules/react-native/Libraries/NativeAnimation`) @@ -510,6 +513,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/logger" react-native-safe-area-context: :path: "../node_modules/react-native-safe-area-context" + react-native-webview: + :path: "../node_modules/react-native-webview" React-perflogger: :path: "../node_modules/react-native/ReactCommon/reactperflogger" React-RCTActionSheet: @@ -577,6 +582,7 @@ SPEC CHECKSUMS: React-jsinspector: 60769e5a0a6d4b32294a2456077f59d0266f9a8b React-logger: 1623c216abaa88974afce404dc8f479406bbc3a0 react-native-safe-area-context: 238cd8b619e05cb904ccad97ef42e84d1b5ae6ec + react-native-webview: 8fc09f66a1a5b16bbe37c3878fda27d5982bb776 React-perflogger: 8c79399b0500a30ee8152d0f9f11beae7fc36595 React-RCTActionSheet: 7316773acabb374642b926c19aef1c115df5c466 React-RCTAnimation: 5341e288375451297057391227f691d9b2326c3d diff --git a/demos/demo-react-native/package.json b/demos/demo-react-native/package.json index d755d6838e..84486f0039 100644 --- a/demos/demo-react-native/package.json +++ b/demos/demo-react-native/package.json @@ -7,16 +7,19 @@ "ios": "react-native run-ios", "start": "react-native start", "test": "jest", - "lint": "eslint . --ext .js,.jsx,.ts,.tsx" + "lint": "eslint . --ext .js,.jsx,.ts,.tsx", + "postinstall": "./scripts/copy-npm-module-to-web-bundles.sh" }, "dependencies": { "@react-navigation/native": "^6.1.0", "@react-navigation/native-stack": "^6.9.5", "theta-client-react-native": "1.5.0", "react": "18.2.0", - "react-native": "0.70.6", + "react-native": "0.71.14", "react-native-safe-area-context": "^4.4.1", - "react-native-screens": "^3.18.2" + "react-native-screens": "^3.18.2", + "marzipano": "0.10.2", + "react-native-webview": "^13.6.2" }, "devDependencies": { "@babel/core": "^7.12.9", diff --git a/demos/demo-react-native/scripts/copy-npm-module-to-web-bundles.sh b/demos/demo-react-native/scripts/copy-npm-module-to-web-bundles.sh new file mode 100755 index 0000000000..303175d0dc --- /dev/null +++ b/demos/demo-react-native/scripts/copy-npm-module-to-web-bundles.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +set -e + +cp node_modules/marzipano/dist/marzipano.js web/Web.bundle/live-preview/marzipano.js diff --git a/demos/demo-react-native/src/Styles.tsx b/demos/demo-react-native/src/Styles.tsx index ec593c8521..932bffd35e 100644 --- a/demos/demo-react-native/src/Styles.tsx +++ b/demos/demo-react-native/src/Styles.tsx @@ -37,9 +37,21 @@ const styles = StyleSheet.create({ }, takePhotoBack: { flex: 1, - alignItems: 'center', backgroundColor: 'white', }, + livePreviewContainer: { + flex: 1, + }, + livePreviewWebview: { + width: '100%', + height: '100%', + }, + livePreviewBottomContainer: { + position: 'absolute', + bottom: 0, + width: '100%', + alignItems: 'center', + }, takePhoto: { width: '100%', height: '100%', @@ -48,10 +60,6 @@ const styles = StyleSheet.create({ zIndex: 0, elevation: 0, }, - shutterBack: { - flex: 1, - justifyContent: 'flex-end', - }, shutter: { alignSelf: 'center', width: 40, diff --git a/demos/demo-react-native/src/TakePhoto.tsx b/demos/demo-react-native/src/TakePhoto.tsx index d395a0b7a4..df02888ae1 100644 --- a/demos/demo-react-native/src/TakePhoto.tsx +++ b/demos/demo-react-native/src/TakePhoto.tsx @@ -1,10 +1,11 @@ import React from 'react'; import { View, - Image, TouchableOpacity, NativeModules, NativeEventEmitter, + Alert, + Platform, } from 'react-native'; import styles from './Styles'; import { @@ -12,47 +13,102 @@ import { stopLivePreview, getPhotoCaptureBuilder, THETA_EVENT_NAME, + isInitialized, } from 'theta-client-react-native'; import {useIsFocused} from '@react-navigation/native'; - -const startLivePreview = async () => { - getLivePreview().then(x => { - console.log(`live preview done with ${x}`); - }); -}; +import WebView from 'react-native-webview'; const TakePhoto = ({navigation}) => { - const [dataUrl, setDataUrl] = React.useState(); const isFocused = useIsFocused(); + const [dataUrl, setDataUrl] = React.useState(); + const [isLoaded, setLoaded] = React.useState(false); + const webViewRef = React.useRef(null); + const source = + Platform.OS === 'android' + ? 'file:///android_asset/live-preview/index.html' + : './Web.bundle/live-preview/index.html'; - const onShutter = async () => { - stopLivePreview(); - const photoCapture = await getPhotoCaptureBuilder().build(); - const url = await photoCapture.takePicture(); - const urls = url.split('/'); - navigation.navigate('sphere', { - item: {fileUrl: url, name: urls[urls.length - 1]}, - }); + const startLivePreview = async () => { + getLivePreview() + .then(x => { + console.log(`live preview done with ${x}`); + }) + .catch(error => { + Alert.alert('getLivePreview', 'error: \n' + JSON.stringify(error), [ + {text: 'OK'}, + ]); + }); + }; + + const setFrameData = (data: string) => { + if (isLoaded && webViewRef.current) { + webViewRef.current.injectJavaScript(` + setFrame('${data}');true + `); + } + }; + + const onLoad = () => { + console.log('onLoad'); + setLoaded(true); }; React.useEffect(() => { if (isFocused) { - const emitter = new NativeEventEmitter(NativeModules.ThetaClientReactNative); + const emitter = new NativeEventEmitter( + NativeModules.ThetaClientReactNative, + ); const eventListener = emitter.addListener(THETA_EVENT_NAME, event => { setDataUrl(event.data); }); startLivePreview(); return () => { - stopLivePreview(); + isInitialized().then(isInit => { + if (isInit) { + stopLivePreview(); + } + }); eventListener.remove(); }; } + return; }, [isFocused]); + React.useEffect(() => { + if (isLoaded && dataUrl) { + setFrameData(dataUrl); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dataUrl, isLoaded]); + + const onShutter = async () => { + stopLivePreview(); + const photoCapture = await getPhotoCaptureBuilder().build(); + const url = await photoCapture.takePicture(); + if (url) { + const urls = url.split('/'); + navigation.navigate('sphere', { + item: {fileUrl: url, name: urls[urls.length - 1]}, + }); + } else { + Alert.alert('takePicture canceled.', undefined, [ + {text: 'OK', onPress: () => startLivePreview}, + ]); + } + }; + return ( - - + + + + diff --git a/demos/demo-react-native/web/Web.bundle/live-preview/index.html b/demos/demo-react-native/web/Web.bundle/live-preview/index.html new file mode 100644 index 0000000000..fd7d422c43 --- /dev/null +++ b/demos/demo-react-native/web/Web.bundle/live-preview/index.html @@ -0,0 +1,16 @@ + + + +Live Preview + + + + + + + +
+ + + + diff --git a/demos/demo-react-native/web/Web.bundle/live-preview/index.js b/demos/demo-react-native/web/Web.bundle/live-preview/index.js new file mode 100644 index 0000000000..50c4e29b8e --- /dev/null +++ b/demos/demo-react-native/web/Web.bundle/live-preview/index.js @@ -0,0 +1,61 @@ +'use strict'; + +/* global Marzipano */ + +let _imageAsset = null; + +function createImageAsset() { + let imageTag = document.createElement('img'); + imageTag.crossOrigin = 'anonymous'; + let asset = new Marzipano.DynamicAsset(imageTag); + return asset; +} + +// eslint-disable-next-line no-unused-vars +function setFrame(frameData) { + if (_imageAsset == null || _imageAsset.element() == null) { + return; + } + + let element = _imageAsset.element(); + if (element.onload) { + return; + } + element.onload = () => { + _imageAsset.markDirty(); + element.onload = null; + }; + element.onerror = () => { + element.onload = null; + }; + + element.src = frameData; +} + +// Create viewer. +var viewer = new Marzipano.Viewer(document.getElementById('pano')); + +// Create source. +_imageAsset = createImageAsset(); +const source = new Marzipano.SingleAssetSource(_imageAsset); + +// Create geometry. +var geometry = new Marzipano.EquirectGeometry([{width: 4000}]); + +// Create view. +var limiter = Marzipano.RectilinearView.limit.traditional( + 1024, + (100 * Math.PI) / 180, +); +var view = new Marzipano.RectilinearView({yaw: Math.PI}, limiter); + +// Create scene. +var scene = viewer.createScene({ + source: source, + geometry: geometry, + view: view, + pinFirstLevel: true, +}); + +// Display scene. +scene.switchTo(); diff --git a/demos/demo-react-native/web/Web.bundle/live-preview/reset.css b/demos/demo-react-native/web/Web.bundle/live-preview/reset.css new file mode 100644 index 0000000000..ed11813c4e --- /dev/null +++ b/demos/demo-react-native/web/Web.bundle/live-preview/reset.css @@ -0,0 +1,48 @@ +/* http://meyerweb.com/eric/tools/css/reset/ + v2.0 | 20110126 + License: none (public domain) +*/ + +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +menu, nav, output, ruby, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} +/* HTML5 display-role reset for older browsers */ +article, aside, details, figcaption, figure, +footer, header, hgroup, menu, nav, section { + display: block; +} +body { + line-height: 1; +} +ol, ul { + list-style: none; +} +blockquote, q { + quotes: none; +} +blockquote:before, blockquote:after, +q:before, q:after { + content: ''; + content: none; +} +table { + border-collapse: collapse; + border-spacing: 0; +} diff --git a/demos/demo-react-native/web/Web.bundle/live-preview/style.css b/demos/demo-react-native/web/Web.bundle/live-preview/style.css new file mode 100644 index 0000000000..145dd40d40 --- /dev/null +++ b/demos/demo-react-native/web/Web.bundle/live-preview/style.css @@ -0,0 +1,28 @@ +* { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + user-select: none; + -webkit-user-drag: none; + -webkit-touch-callout: none; + -ms-content-zooming: none; +} + +html, body { + width: 100%; + height: 100%; + padding: 0; + margin: 0; + overflow: hidden; +} + +#pano { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} \ No newline at end of file diff --git a/demos/demo-react-native/yarn.lock b/demos/demo-react-native/yarn.lock index 9a70bb6f0f..b6b90dc948 100644 --- a/demos/demo-react-native/yarn.lock +++ b/demos/demo-react-native/yarn.lock @@ -2031,6 +2031,11 @@ bl@^4.1.0: inherits "^2.0.4" readable-stream "^3.4.0" +bowser@1.9.4: + version "1.9.4" + resolved "https://registry.yarnpkg.com/bowser/-/bowser-1.9.4.tgz#890c58a2813a9d3243704334fa81b96a5c150c9a" + integrity sha512-9IdMmj2KjigRq6oWhmwv1W36pDuA4STQZ8q6YO9um+x07xgYNCD3Oou+WP/3L1HNz7iqythGet3/p4wvc8AAwQ== + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -2648,16 +2653,16 @@ escape-html@~1.0.3: resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== +escape-string-regexp@2.0.0, escape-string-regexp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" + integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== + escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== -escape-string-regexp@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" - integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== - escape-string-regexp@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" @@ -3142,6 +3147,11 @@ get-symbol-description@^1.0.0: call-bind "^1.0.2" get-intrinsic "^1.1.1" +gl-matrix@3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/gl-matrix/-/gl-matrix-3.3.0.tgz#232eef60b1c8b30a28cbbe75b2caf6c48fd6358b" + integrity sha512-COb7LDz+SXaHtl/h4LeaFcNdJdAQSDeVqjiIihSXNrkWObZLhDI4hIkZC11Aeqp7bcE72clzB0BnDXr2SmslRA== + glob-parent@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" @@ -3216,6 +3226,11 @@ graphemer@^1.4.0: resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== +hammerjs@2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/hammerjs/-/hammerjs-2.0.4.tgz#e161706d2e610ef295b16eadc515df7d9c59aa23" + integrity sha512-7eWteNDFD5a792W0bsxLUfowijwXFIwN8JDuZ3e8+0+mQQ9Hgp0ROxr36DdhbjACwi5c0ep86loTI2NcT41ang== + has-bigints@^1.0.1, has-bigints@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" @@ -3368,7 +3383,7 @@ internal-slot@^1.0.5: hasown "^2.0.0" side-channel "^1.0.4" -invariant@^2.2.4: +invariant@2.2.4, invariant@^2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== @@ -4368,6 +4383,16 @@ makeerror@1.0.12: dependencies: tmpl "1.0.5" +marzipano@0.10.2: + version "0.10.2" + resolved "https://registry.yarnpkg.com/marzipano/-/marzipano-0.10.2.tgz#a1c51a24171909a2ed5dd4bdb8d994b958313f67" + integrity sha512-YWgxpnZa8rlZWT4tkV9LF3abqwMeS3sphkbssWlM1EqZZ/XdlFJVL18iR4wI+uUnvVWjMmjAKDn/lPlOo19Tsg== + dependencies: + bowser "1.9.4" + gl-matrix "3.3.0" + hammerjs "2.0.4" + minimal-event-emitter "1.0.0" + memoize-one@^5.0.0: version "5.2.1" resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" @@ -4835,6 +4860,11 @@ mimic-fn@^2.1.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== +minimal-event-emitter@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/minimal-event-emitter/-/minimal-event-emitter-1.0.0.tgz#9ce6203aaa1da7d6b62206c78a670212847e605b" + integrity sha512-jZFCw+HUAgVWGSnCpff4NaB1PRwNcnIYRN1hgaA1QfOxG5lBhNmqpmcVW/o2+28Pc8CxPImsipDNrfUQhHCukw== + minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" @@ -5402,6 +5432,14 @@ react-native-screens@^3.18.2: react-freeze "^1.0.0" warn-once "^0.1.0" +react-native-webview@^13.6.2: + version "13.6.2" + resolved "https://registry.yarnpkg.com/react-native-webview/-/react-native-webview-13.6.2.tgz#0a9b18793e915add5b5dbdbf32509d7751b49167" + integrity sha512-QzhQ5JCU+Nf2W285DtvCZOVQy/MkJXMwNDYPZvOWQbAOgxJMSSO+BtqXTMA1UPugDsko6PxJ0TxSlUwIwJijDg== + dependencies: + escape-string-regexp "2.0.0" + invariant "2.2.4" + react-native@0.70.6: version "0.70.6" resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.70.6.tgz#d692f8b51baffc28e1a8bc5190cdb779de937aa8" diff --git a/docs/assets/screen_capture.jpg b/docs/assets/screen_capture.jpg new file mode 100644 index 0000000000..5b64f6092e Binary files /dev/null and b/docs/assets/screen_capture.jpg differ diff --git a/docs/assets/screen_info.jpg b/docs/assets/screen_info.jpg new file mode 100644 index 0000000000..800fe9759e Binary files /dev/null and b/docs/assets/screen_info.jpg differ diff --git a/docs/assets/screen_menu.jpg b/docs/assets/screen_menu.jpg new file mode 100644 index 0000000000..a8def013db Binary files /dev/null and b/docs/assets/screen_menu.jpg differ 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 f05b26a29d..eb3a0645ca 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 @@ -2,12 +2,7 @@ package com.ricoh360.thetaclient.theta_client_flutter import com.ricoh360.thetaclient.DigestAuth import com.ricoh360.thetaclient.ThetaRepository.* -import com.ricoh360.thetaclient.capture.Capture -import com.ricoh360.thetaclient.capture.PhotoCapture -import com.ricoh360.thetaclient.capture.TimeShiftCapture -import com.ricoh360.thetaclient.capture.VideoCapture -import com.ricoh360.thetaclient.capture.LimitlessIntervalCapture -import com.ricoh360.thetaclient.capture.ShotCountSpecifiedIntervalCapture +import com.ricoh360.thetaclient.capture.* import io.flutter.plugin.common.MethodCall const val KEY_CLIENT_MODE = "clientMode" @@ -107,6 +102,46 @@ fun toResult(fileInfoList: List): List> { return result } +fun toAutoBracket(list: List>): BracketSettingList { + val autoBracket = BracketSettingList() + + list.forEach { map -> + autoBracket.add( + BracketSetting( + aperture = (map["aperture"] as? String)?.let { ApertureEnum.valueOf(it) }, + colorTemperature = map["colorTemperature"] as? Int, + exposureCompensation = (map["exposureCompensation"] as? String)?.let { ExposureCompensationEnum.valueOf(it) }, + exposureProgram = (map["exposureProgram"] as? String)?.let { ExposureProgramEnum.valueOf(it) }, + iso = (map["iso"] as? String)?.let { IsoEnum.valueOf(it) }, + shutterSpeed = (map["shutterSpeed"] as? String)?.let { ShutterSpeedEnum.valueOf(it) }, + whiteBalance = (map["whiteBalance"] as? String)?.let { WhiteBalanceEnum.valueOf(it) }, + ) + ) + } + + return autoBracket +} + +fun toResult(autoBracket: BracketSettingList): List> { + val resultList: MutableList> = mutableListOf() + + autoBracket.list.forEach { bracketSetting -> + resultList.add( + mapOf( + "aperture" to bracketSetting.aperture?.name, + "colorTemperature" to bracketSetting.colorTemperature, + "exposureCompensation" to bracketSetting.exposureCompensation?.name, + "exposureProgram" to bracketSetting.exposureProgram?.name, + "iso" to bracketSetting.iso?.name, + "shutterSpeed" to bracketSetting.shutterSpeed?.name, + "whiteBalance" to bracketSetting.whiteBalance?.name + ) + ) + } + + return resultList +} + fun toBurstOption(map: Map): BurstOption { return BurstOption( burstCaptureNum = (map["burstCaptureNum"] as? String)?.let { BurstCaptureNumEnum.valueOf(it) }, @@ -287,6 +322,17 @@ fun setShotCountSpecifiedIntervalCaptureBuilderParams(call: MethodCall, builder: } } +fun setCompositeIntervalCaptureBuilderParams(call: MethodCall, builder: CompositeIntervalCapture.Builder) { + call.argument("_capture_interval")?.let { + if (it >= 0) { + builder.setCheckStatusCommandInterval(it.toLong()) + } + } + call.argument(OptionNameEnum.CompositeShootingOutputInterval.name)?.also { + builder.setCompositeShootingOutputInterval(it) + } +} + fun toGetOptionsParam(data: List): List { val optionNames = mutableListOf() data.forEach { name -> @@ -358,7 +404,11 @@ fun toResult(options: Options): Map { OptionNameEnum.Username ) OptionNameEnum.values().forEach { name -> - if (name == OptionNameEnum.Bitrate) { + if (name == OptionNameEnum.AutoBracket) { + options.getValue(OptionNameEnum.AutoBracket)?.let { autoBracket -> + result[OptionNameEnum.AutoBracket.name] = toResult(autoBracket) + } + } else if (name == OptionNameEnum.Bitrate) { options.getValue(OptionNameEnum.Bitrate)?.let { bitrate -> if (bitrate is BitrateEnum) { result[OptionNameEnum.Bitrate.name] = bitrate.toString() @@ -440,6 +490,9 @@ fun setOptionValue(options: Options, name: OptionNameEnum, value: Any) { optionValue = value.toLong() } options.setValue(name, optionValue) + } else if (name == OptionNameEnum.AutoBracket) { + @Suppress("UNCHECKED_CAST") + options.setValue(name, toAutoBracket(value as List>)) } else if (name == OptionNameEnum.Bitrate) { @Suppress("UNCHECKED_CAST") if (value is Int) { 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 1b7cf9f5a8..ef56531607 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 @@ -2,15 +2,7 @@ package com.ricoh360.thetaclient.theta_client_flutter import android.util.Log import com.ricoh360.thetaclient.ThetaRepository -import com.ricoh360.thetaclient.capture.PhotoCapture -import com.ricoh360.thetaclient.capture.TimeShiftCapture -import com.ricoh360.thetaclient.capture.TimeShiftCapturing -import com.ricoh360.thetaclient.capture.VideoCapture -import com.ricoh360.thetaclient.capture.VideoCapturing -import com.ricoh360.thetaclient.capture.LimitlessIntervalCapture -import com.ricoh360.thetaclient.capture.LimitlessIntervalCapturing -import com.ricoh360.thetaclient.capture.ShotCountSpecifiedIntervalCapture -import com.ricoh360.thetaclient.capture.ShotCountSpecifiedIntervalCapturing +import com.ricoh360.thetaclient.capture.* import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.MethodCall @@ -51,6 +43,9 @@ class ThetaClientFlutterPlugin : FlutterPlugin, MethodCallHandler { var shotCountSpecifiedIntervalCaptureBuilder: ShotCountSpecifiedIntervalCapture.Builder? = null var shotCountSpecifiedIntervalCapture: ShotCountSpecifiedIntervalCapture? = null var shotCountSpecifiedIntervalCapturing: ShotCountSpecifiedIntervalCapturing? = null + var compositeIntervalCaptureBuilder: CompositeIntervalCapture.Builder? = null + var compositeIntervalCapture: CompositeIntervalCapture? = null + var compositeIntervalCapturing: CompositeIntervalCapturing? = null companion object { const val errorCode: String = "Error" @@ -60,11 +55,14 @@ class ThetaClientFlutterPlugin : FlutterPlugin, MethodCallHandler { const val eventNameNotify = "theta_client_flutter/theta_notify" const val notifyIdLivePreview = 10001 - const val notifyIdTimeShiftProgress = 10002 + const val notifyIdTimeShiftProgress = 10011 + const val notifyIdTimeShiftStopError = 10012 const val notifyIdVideoCaptureStopError = 10003 const val notifyIdLimitlessIntervalCaptureStopError = 10004 - const val notifyIdShotCountSpecifiedIntervalCaptureProgress = 10005 - const val notifyIdShotCountSpecifiedIntervalCaptureStopError = 10006 + const val notifyIdShotCountSpecifiedIntervalCaptureProgress = 10021 + const val notifyIdShotCountSpecifiedIntervalCaptureStopError = 10022 + const val notifyIdCompositeIntervalCaptureProgress = 10031; + const val notifyIdCompositeIntervalCaptureStopError = 10032; } fun sendNotifyEvent(id: Int, params: Map) { @@ -255,6 +253,24 @@ class ThetaClientFlutterPlugin : FlutterPlugin, MethodCallHandler { stopShotCountSpecifiedIntervalCapture(result) } + "getCompositeIntervalCaptureBuilder" -> { + getCompositeIntervalCaptureBuilder(call, result) + } + + "buildCompositeIntervalCapture" -> { + scope.launch { + buildCompositeIntervalCapture(call, result) + } + } + + "startCompositeIntervalCapture" -> { + startCompositeIntervalCapture(result) + } + + "stopCompositeIntervalCapture" -> { + stopCompositeIntervalCapture(result) + } + "getOptions" -> { scope.launch { getOptions(call, result) @@ -426,6 +442,9 @@ class ThetaClientFlutterPlugin : FlutterPlugin, MethodCallHandler { shotCountSpecifiedIntervalCaptureBuilder = null shotCountSpecifiedIntervalCapture = null shotCountSpecifiedIntervalCapturing = null + compositeIntervalCaptureBuilder = null + compositeIntervalCapture = null + compositeIntervalCapturing = null try { endpoint = call.argument("endpoint")!! @@ -599,10 +618,17 @@ class ThetaClientFlutterPlugin : FlutterPlugin, MethodCallHandler { } timeShiftCapturing = timeShiftCapture.startCapture(object : TimeShiftCapture.StartCaptureCallback { - override fun onError(exception: ThetaRepository.ThetaRepositoryException) { + override fun onCaptureFailed(exception: ThetaRepository.ThetaRepositoryException) { result.error(exception.javaClass.simpleName, exception.message, null) } + override fun onStopFailed(exception: ThetaRepository.ThetaRepositoryException) { + sendNotifyEvent( + notifyIdTimeShiftStopError, + toMessageNotifyParam(exception.message ?: exception.toString()) + ) + } + override fun onProgress(completion: Float) { sendNotifyEvent( notifyIdTimeShiftProgress, @@ -610,7 +636,7 @@ class ThetaClientFlutterPlugin : FlutterPlugin, MethodCallHandler { ) } - override fun onSuccess(fileUrl: String?) { + override fun onCaptureCompleted(fileUrl: String?) { result.success(fileUrl) } }) @@ -818,6 +844,79 @@ class ThetaClientFlutterPlugin : FlutterPlugin, MethodCallHandler { result.success(null) } + fun getCompositeIntervalCaptureBuilder(call: MethodCall, result: Result) { + val theta = thetaRepository + if (theta == null) { + result.error(errorCode, messageNotInit, null) + return + } + (call.arguments as? Int)?.let { + compositeIntervalCaptureBuilder = theta.getCompositeIntervalCaptureBuilder(it) + } + result.success(null) + } + + suspend fun buildCompositeIntervalCapture(call: MethodCall, result: Result) { + val theta = thetaRepository + val compositeIntervalCaptureBuilder = compositeIntervalCaptureBuilder + if (theta == null || compositeIntervalCaptureBuilder == null) { + result.error(errorCode, messageNotInit, null) + return + } + setCaptureBuilderParams(call, compositeIntervalCaptureBuilder) + setCompositeIntervalCaptureBuilderParams(call, compositeIntervalCaptureBuilder) + try { + compositeIntervalCapture = compositeIntervalCaptureBuilder.build() + result.success(null) + } catch (e: Exception) { + result.error(e.javaClass.simpleName, e.message, null) + } + } + + fun startCompositeIntervalCapture(result: Result) { + val theta = thetaRepository + val compositeIntervalCapture = compositeIntervalCapture + if (theta == null || compositeIntervalCapture == null) { + result.error(errorCode, messageNotInit, null) + return + } + compositeIntervalCapturing = + compositeIntervalCapture.startCapture(object : CompositeIntervalCapture.StartCaptureCallback { + override fun onCaptureFailed(exception: ThetaRepository.ThetaRepositoryException) { + result.error(exception.javaClass.simpleName, exception.message, null) + } + + override fun onStopFailed(exception: ThetaRepository.ThetaRepositoryException) { + sendNotifyEvent( + notifyIdCompositeIntervalCaptureStopError, + toMessageNotifyParam(exception.message ?: exception.toString()) + ) + } + + override fun onProgress(completion: Float) { + sendNotifyEvent( + notifyIdCompositeIntervalCaptureProgress, + toCaptureProgressNotifyParam(completion) + ) + } + + override fun onCaptureCompleted(fileUrls: List?) { + result.success(fileUrls) + } + }) + } + + fun stopCompositeIntervalCapture(result: Result) { + val theta = thetaRepository + val compositeIntervalCapturing = compositeIntervalCapturing + if (theta == null || compositeIntervalCapturing == null) { + result.error(errorCode, messageNotInit, null) + return + } + compositeIntervalCapturing.stopCapture() + result.success(null) + } + suspend fun listFiles(call: MethodCall, result: Result) { if (thetaRepository == null) { result.error(errorCode, messageNotInit, null) diff --git a/flutter/ios/Classes/ConvertUtil.swift b/flutter/ios/Classes/ConvertUtil.swift index 527c09f465..09e6724fbf 100644 --- a/flutter/ios/Classes/ConvertUtil.swift +++ b/flutter/ios/Classes/ConvertUtil.swift @@ -264,6 +264,111 @@ func setShotCountSpecifiedIntervalCaptureBuilderParams(params: [String: Any], bu } } +func toAutoBracket(params: [[String: Any]]) -> ThetaRepository.BracketSettingList { + let autoBracket = ThetaRepository.BracketSettingList(list: NSMutableArray()) + + params.forEach { map in + let aperture = { + if let name = map["aperture"] as? String { + return getEnumValue(values: ThetaRepository.ApertureEnum.values(), name: name) + } else { + return nil + } + }() + + let colorTemperature = { + if let value = map["colorTemperature"] as? Int { + return toKotlinInt(value: value) + } else { + return nil + } + }() + + let exposureCompensation = { + if let name = map["exposureCompensation"] as? String { + return getEnumValue(values: ThetaRepository.ExposureCompensationEnum.values(), name: name) + } else { + return nil + } + }() + + let exposureProgram = { + if let name = map["exposureProgram"] as? String { + return getEnumValue(values: ThetaRepository.ExposureProgramEnum.values(), name: name) + } else { + return nil + } + }() + + let iso = { + if let name = map["iso"] as? String { + return getEnumValue(values: ThetaRepository.IsoEnum.values(), name: name) + } else { + return nil + } + }() + + let shutterSpeed = { + if let name = map["shutterSpeed"] as? String { + return getEnumValue(values: ThetaRepository.ShutterSpeedEnum.values(), name: name) + } else { + return nil + } + }() + + let whiteBalance = { + if let name = map["whiteBalance"] as? String { + return getEnumValue(values: ThetaRepository.WhiteBalanceEnum.values(), name: name) + } else { + return nil + } + }() + + autoBracket.add(setting: ThetaRepository.BracketSetting( + aperture: aperture, + colorTemperature: colorTemperature, + exposureCompensation: exposureCompensation, + exposureProgram: exposureProgram, + iso: iso, + shutterSpeed: shutterSpeed, + whiteBalance: whiteBalance + )) + } + + return autoBracket +} + +func convertResult(autoBracket: ThetaRepository.BracketSettingList) -> [[String: Any]] { + var resultList = [[String: Any]]() + + autoBracket.list.forEach { bracketSetting in + if let setting = bracketSetting as? ThetaRepository.BracketSetting { + resultList.append([ + "aperture": setting.aperture?.name, + "colorTemperature": setting.colorTemperature?.intValue, + "exposureCompensation": setting.exposureCompensation?.name, + "exposureProgram": setting.exposureProgram?.name, + "iso": setting.iso?.name, + "shutterSpeed": setting.shutterSpeed?.name, + "whiteBalance": setting.whiteBalance?.name, + ]) + } + } + + return resultList +} + +func setCompositeIntervalCaptureBuilderParams(params: [String: Any], builder: CompositeIntervalCapture.Builder) { + if let interval = params["_capture_interval"] as? Int, + interval >= 0 + { + builder.setCheckStatusCommandInterval(timeMillis: Int64(interval)) + } + if let value = params[ThetaRepository.OptionNameEnum.compositeshootingoutputinterval.name] as? Int32 { + builder.setCompositeShootingOutputInterval(sec: value) + } +} + func toBitrate(value: Any) -> ThetaRepositoryBitrate? { if value is NSNumber, let intVal = value as? Int32 { return ThetaRepository.BitrateNumber(value: intVal) @@ -432,6 +537,8 @@ func convertResult(options: ThetaRepository.Options) -> [String: Any] { result[name.name] = value } else if let bitrate = value as? ThetaRepository.BitrateNumber { result[name.name] = bitrate.value + } else if value is ThetaRepository.BracketSettingList, let autoBracket = value as? ThetaRepository.BracketSettingList { + result[name.name] = convertResult(autoBracket: autoBracket) } else if value is ThetaRepository.BurstOption, let burstOption = value as? ThetaRepository.BurstOption { result[name.name] = convertResult(burstOption: burstOption) } else if value is ThetaRepository.GpsInfo { @@ -484,6 +591,10 @@ func setOptionsValue(options: ThetaRepository.Options, name: String, value: Any) options.aiAutoThumbnail = getEnumValue(values: ThetaRepository.AiAutoThumbnailEnum.values(), name: value as! String)! case ThetaRepository.OptionNameEnum.aperture.name: options.aperture = getEnumValue(values: ThetaRepository.ApertureEnum.values(), name: value as! String)! + case ThetaRepository.OptionNameEnum.autobracket.name: + if let params = value as? [[String:Any]] { + options.autoBracket = toAutoBracket(params: params) + } case ThetaRepository.OptionNameEnum.bitrate.name: options.bitrate = toBitrate(value: value) case ThetaRepository.OptionNameEnum.bluetoothpower.name: diff --git a/flutter/ios/Classes/SwiftThetaClientFlutterPlugin.swift b/flutter/ios/Classes/SwiftThetaClientFlutterPlugin.swift index fff2b24976..174dd4ab90 100644 --- a/flutter/ios/Classes/SwiftThetaClientFlutterPlugin.swift +++ b/flutter/ios/Classes/SwiftThetaClientFlutterPlugin.swift @@ -4,11 +4,14 @@ import UIKit let EVENT_NOTIFY = "theta_client_flutter/theta_notify" let NOTIFY_LIVE_PREVIEW = 10001 -let NOTIFY_TIME_SHIFT_PROGRESS = 10002 +let NOTIFY_TIME_SHIFT_PROGRESS = 10011 +let NOTIFY_TIME_SHIFT_STOP_ERROR = 10011 let NOTIFY_VIDEO_CAPTURE_STOP_ERROR = 10003 let NOTIFY_LIMITLESS_INTERVAL_CAPTURE_STOP_ERROR = 10004 -let NOTIFY_SHOT_COUNT_SPECIFIED_INTERVAL_CAPTURE_PROGRESS = 10005 -let NOTIFY_SHOT_COUNT_SPECIFIED_INTERVAL_CAPTURE_STOP_ERROR = 10006 +let NOTIFY_SHOT_COUNT_SPECIFIED_INTERVAL_CAPTURE_PROGRESS = 10021 +let NOTIFY_SHOT_COUNT_SPECIFIED_INTERVAL_CAPTURE_STOP_ERROR = 10022 +let NOTIFY_COMPOSITE_INTERVAL_PROGRESS = 10031 +let NOTIFY_COMPOSITE_INTERVAL_STOP_ERROR = 10032 public class SwiftThetaClientFlutterPlugin: NSObject, FlutterPlugin, FlutterStreamHandler { public func onListen(withArguments _: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { @@ -44,7 +47,10 @@ public class SwiftThetaClientFlutterPlugin: NSObject, FlutterPlugin, FlutterStre var shotCountSpecifiedIntervalCaptureBuilder: ShotCountSpecifiedIntervalCapture.Builder? = nil var shotCountSpecifiedIntervalCapture: ShotCountSpecifiedIntervalCapture? = nil var shotCountSpecifiedIntervalCapturing: ShotCountSpecifiedIntervalCapturing? = nil - + var compositeIntervalCaptureBuilder: CompositeIntervalCapture.Builder? = nil + var compositeIntervalCapture: CompositeIntervalCapture? = nil + var compositeIntervalCapturing: CompositeIntervalCapturing? = nil + public static func register(with registrar: FlutterPluginRegistrar) { let channel = FlutterMethodChannel(name: "theta_client_flutter", binaryMessenger: registrar.messenger()) let instance = SwiftThetaClientFlutterPlugin() @@ -130,6 +136,14 @@ public class SwiftThetaClientFlutterPlugin: NSObject, FlutterPlugin, FlutterStre startShotCountSpecifiedIntervalCapture(result: result) case "stopShotCountSpecifiedIntervalCapture": stopShotCountSpecifiedIntervalCapture(result: result) + case "getCompositeIntervalCaptureBuilder": + getCompositeIntervalCaptureBuilder(call: call, result: result) + case "buildCompositeIntervalCapture": + buildCompositeIntervalCapture(call: call, result: result) + case "startCompositeIntervalCapture": + startCompositeIntervalCapture(result: result) + case "stopCompositeIntervalCapture": + stopCompositeIntervalCapture(result: result) case "getOptions": getOptions(call: call, result: result) case "setOptions": @@ -199,6 +213,9 @@ public class SwiftThetaClientFlutterPlugin: NSObject, FlutterPlugin, FlutterStre shotCountSpecifiedIntervalCaptureBuilder = nil shotCountSpecifiedIntervalCapture = nil shotCountSpecifiedIntervalCapturing = nil + compositeIntervalCaptureBuilder = nil + compositeIntervalCapture = nil + compositeIntervalCapturing = nil previewing = false thetaRepository = try await withCheckedThrowingContinuation { continuation in @@ -541,16 +558,21 @@ public class SwiftThetaClientFlutterPlugin: NSObject, FlutterPlugin, FlutterStre self.callback = callback self.plugin = plugin } - - func onError(exception: ThetaRepository.ThetaRepositoryException) { + + func onCaptureFailed(exception: ThetaRepository.ThetaRepositoryException) { callback(nil, exception.asError()) } - + + func onStopFailed(exception: ThetaRepository.ThetaRepositoryException) { + let error = exception.asError() + plugin?.sendNotifyEvent(id: NOTIFY_TIME_SHIFT_STOP_ERROR, params: toMessageNotifyParam(message: error.localizedDescription)) + } + func onProgress(completion: Float) { plugin?.sendNotifyEvent(id: NOTIFY_TIME_SHIFT_PROGRESS, params: toCaptureProgressNotifyParam(value: completion)) } - - func onSuccess(fileUrl: String?) { + + func onCaptureCompleted(fileUrl: String?) { callback(fileUrl, nil) } } @@ -828,6 +850,94 @@ public class SwiftThetaClientFlutterPlugin: NSObject, FlutterPlugin, FlutterStre capturing.stopCapture() result(nil) } + + func getCompositeIntervalCaptureBuilder(call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let thetaRepository else { + let flutterError = FlutterError(code: SwiftThetaClientFlutterPlugin.errorCode, message: SwiftThetaClientFlutterPlugin.messageNotInit, details: nil) + result(flutterError) + return + } + if let shootingTimeSec = call.arguments as? Int32 { + compositeIntervalCaptureBuilder = thetaRepository.getCompositeIntervalCaptureBuilder(shootingTimeSec: shootingTimeSec) + } + result(nil) + } + + func buildCompositeIntervalCapture(call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let _ = thetaRepository, let builder = compositeIntervalCaptureBuilder else { + let flutterError = FlutterError(code: SwiftThetaClientFlutterPlugin.errorCode, message: SwiftThetaClientFlutterPlugin.messageNotInit, details: nil) + result(flutterError) + return + } + if let arguments = call.arguments as? [String: Any] { + setCaptureBuilderParams(params: arguments, builder: builder) + setCompositeIntervalCaptureBuilderParams(params: arguments, builder: builder) + } + builder.build(completionHandler: { capture, error in + if let thetaError = error { + let flutterError = FlutterError(code: SwiftThetaClientFlutterPlugin.errorCode, message: thetaError.localizedDescription, details: nil) + result(flutterError) + } else { + self.compositeIntervalCapture = capture + result(nil) + } + }) + } + + func startCompositeIntervalCapture(result: @escaping FlutterResult) { + guard let _ = thetaRepository, let capture = compositeIntervalCapture else { + let flutterError = FlutterError(code: SwiftThetaClientFlutterPlugin.errorCode, message: SwiftThetaClientFlutterPlugin.messageNotInit, details: nil) + result(flutterError) + return + } + class Callback: CompositeIntervalCaptureStartCaptureCallback { + let callback: (_ urls: [String]?, _ error: Error?) -> Void + weak var plugin: SwiftThetaClientFlutterPlugin? + init(_ callback: @escaping (_ urls: [String]?, _ error: Error?) -> Void, plugin: SwiftThetaClientFlutterPlugin) { + self.callback = callback + self.plugin = plugin + } + + func onCaptureFailed(exception: ThetaRepository.ThetaRepositoryException) { + callback(nil, exception.asError()) + } + + func onStopFailed(exception: ThetaRepository.ThetaRepositoryException) { + let error = exception.asError() + plugin?.sendNotifyEvent(id: NOTIFY_COMPOSITE_INTERVAL_STOP_ERROR, params: toMessageNotifyParam(message: error.localizedDescription)) + } + + func onProgress(completion: Float) { + plugin?.sendNotifyEvent(id: NOTIFY_COMPOSITE_INTERVAL_PROGRESS, params: toCaptureProgressNotifyParam(value: completion)) + } + + func onCaptureCompleted(fileUrls: [String]?) { + callback(fileUrls, nil) + } + } + + compositeIntervalCapturing = capture.startCapture( + callback: Callback({ fileUrl, error in + if let thetaError = error { + let flutterError = FlutterError(code: SwiftThetaClientFlutterPlugin.errorCode, message: thetaError.localizedDescription, details: nil) + result(flutterError) + } else { + result(fileUrl) + } + }, + plugin: self) + ) + } + + func stopCompositeIntervalCapture(result: @escaping FlutterResult) { + guard let _ = thetaRepository, let capturing = compositeIntervalCapturing else { + let flutterError = FlutterError(code: SwiftThetaClientFlutterPlugin.errorCode, message: SwiftThetaClientFlutterPlugin.messageNotInit, details: nil) + result(flutterError) + return + } + capturing.stopCapture() + result(nil) + } func getOptions(call: FlutterMethodCall, result: @escaping FlutterResult) { if thetaRepository == nil { diff --git a/flutter/lib/capture/capture.dart b/flutter/lib/capture/capture.dart index 7f492ae0d8..039ba4514d 100644 --- a/flutter/lib/capture/capture.dart +++ b/flutter/lib/capture/capture.dart @@ -97,14 +97,14 @@ class TimeShiftCapture extends Capture { /// Starts TimeShift capture. TimeShiftCapturing startCapture( - void Function(String? fileUrl) onSuccess, - void Function(double completion) onProgress, - void Function(Exception exception) onError, - ) { + void Function(String? fileUrl) onCaptureCompleted, + void Function(double completion) onProgress, + void Function(Exception exception) onCaptureFailed, + {void Function(Exception exception)? onStopFailed}) { ThetaClientFlutterPlatform.instance - .startTimeShiftCapture(onProgress) - .then((value) => onSuccess(value)) - .onError((error, stackTrace) => onError(error as Exception)); + .startTimeShiftCapture(onProgress, onStopFailed) + .then((value) => onCaptureCompleted(value)) + .onError((error, stackTrace) => onCaptureFailed(error as Exception)); return TimeShiftCapturing(); } } @@ -186,3 +186,35 @@ class ShotCountSpecifiedIntervalCapture extends Capture { return ShotCountSpecifiedIntervalCapturing(); } } + +/// Capture of interval composite shooting +class CompositeIntervalCapture extends Capture { + final int _interval; + + CompositeIntervalCapture(super.options, this._interval); + + int getCheckStatusCommandInterval() { + return _interval; + } + + /// Get In-progress save interval for interval composite shooting (sec). + int? getCompositeShootingOutputInterval() => + _options[OptionNameEnum.compositeShootingOutputInterval.rawValue]; + + /// Get Shooting time for interval composite shooting (sec). + int? getCompositeShootingTime() => + _options[OptionNameEnum.compositeShootingTime.rawValue]; + + /// Starts interval composite shooting. + CompositeIntervalCapturing startCapture( + void Function(List? fileUrls) onSuccess, + void Function(double completion) onProgress, + void Function(Exception exception) onCaptureFailed, + {void Function(Exception exception)? onStopFailed}) { + ThetaClientFlutterPlatform.instance + .startCompositeIntervalCapture(onProgress, onStopFailed) + .then((value) => onSuccess(value)) + .onError((error, stackTrace) => onCaptureFailed(error as Exception)); + return CompositeIntervalCapturing(); + } +} diff --git a/flutter/lib/capture/capture_builder.dart b/flutter/lib/capture/capture_builder.dart index be529ee787..87e89f6b02 100644 --- a/flutter/lib/capture/capture_builder.dart +++ b/flutter/lib/capture/capture_builder.dart @@ -249,6 +249,37 @@ class ShotCountSpecifiedIntervalCaptureBuilder } } +/// Builder of CompositeIntervalCapture +class CompositeIntervalCaptureBuilder + extends CaptureBuilder { + int _interval = -1; + + CompositeIntervalCaptureBuilder setCheckStatusCommandInterval( + int timeMillis) { + _interval = timeMillis; + return this; + } + + /// Set In-progress save interval for interval composite shooting (sec). + CompositeIntervalCaptureBuilder setCompositeShootingOutputInterval(int sec) { + _options[OptionNameEnum.compositeShootingOutputInterval.rawValue] = sec; + return this; + } + + /// Builds an instance of a CompositeIntervalCapture that has all the combined parameters of the Options that have been added to the Builder. + Future build() async { + var completer = Completer(); + try { + await ThetaClientFlutterPlatform.instance + .buildCompositeIntervalCapture(_options, _interval); + completer.complete(CompositeIntervalCapture(_options, _interval)); + } catch (e) { + completer.completeError(e); + } + return completer.future; + } +} + /// Photo image format used in PhotoCapture. enum PhotoFileFormatEnum { /// Image File format. diff --git a/flutter/lib/capture/capturing.dart b/flutter/lib/capture/capturing.dart index 6fbbf6d39d..fd15d585c0 100644 --- a/flutter/lib/capture/capturing.dart +++ b/flutter/lib/capture/capturing.dart @@ -9,7 +9,7 @@ abstract class Capturing { /// TimeShiftCapturing class TimeShiftCapturing extends Capturing { /// Stops TimeShift capture. - /// When call stopCapture() then call property callback. + /// When call stopCapture() then call property callback. @override void stopCapture() { ThetaClientFlutterPlatform.instance.stopTimeShiftCapture(); @@ -19,7 +19,7 @@ class TimeShiftCapturing extends Capturing { /// VideoCapturing class VideoCapturing extends Capturing { /// Stops video capture. - /// When call stopCapture() then call property callback. + /// When call stopCapture() then call property callback. @override void stopCapture() { ThetaClientFlutterPlatform.instance.stopVideoCapture(); @@ -29,7 +29,7 @@ class VideoCapturing extends Capturing { /// LimitlessIntervalCapturing class LimitlessIntervalCapturing extends Capturing { /// Stops limitless interval capture. - /// When call stopCapture() then call property callback. + /// When call stopCapture() then call property callback. @override void stopCapture() { ThetaClientFlutterPlatform.instance.stopLimitlessIntervalCapture(); @@ -39,9 +39,19 @@ class LimitlessIntervalCapturing extends Capturing { /// ShotCountSpecifiedIntervalCapturing class ShotCountSpecifiedIntervalCapturing extends Capturing { /// Stops interval shooting with the shot count specified. - /// When call stopCapture() then call property callback. + /// When call stopCapture() then call property callback. @override void stopCapture() { ThetaClientFlutterPlatform.instance.stopShotCountSpecifiedIntervalCapture(); } } + +/// CompositeIntervalCapturing +class CompositeIntervalCapturing extends Capturing { + /// Stops interval composite shooting. + /// When call stopCapture() then call property callback. + @override + void stopCapture() { + ThetaClientFlutterPlatform.instance.stopCompositeIntervalCapture(); + } +} diff --git a/flutter/lib/theta_client_flutter.dart b/flutter/lib/theta_client_flutter.dart index e2b066af0a..ed48d9fa90 100644 --- a/flutter/lib/theta_client_flutter.dart +++ b/flutter/lib/theta_client_flutter.dart @@ -154,6 +154,14 @@ class ThetaClientFlutter { return ShotCountSpecifiedIntervalCaptureBuilder(); } + /// Get getCompositeIntervalCapture.Builder for capture interval composite shooting. + CompositeIntervalCaptureBuilder + getCompositeIntervalCaptureBuilder(int shootingTimeSec) { + ThetaClientFlutterPlatform.instance + .getCompositeIntervalCaptureBuilder(shootingTimeSec); + return CompositeIntervalCaptureBuilder(); + } + /// Acquires the properties and property support specifications for shooting, the camera, etc. /// /// Refer to the [options category](https://github.com/ricohapi/theta-api-specs/blob/main/theta-web-api-v2.1/options.md) @@ -1483,6 +1491,9 @@ enum OptionNameEnum { /// Option name _aiAutoThumbnail aiAutoThumbnail('AiAutoThumbnail', AiAutoThumbnailEnum), + /// Option name _autoBracket + autoBracket('AutoBracket', List), + /// Option name aperture aperture('Aperture', ApertureEnum), @@ -1821,6 +1832,53 @@ class BitrateNumber extends Bitrate { BitrateNumber(this.value) : super._internal(value.toString()); } +/// BracketSetting value. +class BracketSetting { + /// Aperture value. + final ApertureEnum? aperture; + + /// Color temperature of the camera (Kelvin). + final int? colorTemperature; + + /// Exposure compensation (EV). + final ExposureCompensationEnum? exposureCompensation; + + /// Exposure program. + final ExposureProgramEnum? exposureProgram; + + /// ISO sensitivity. + final IsoEnum? iso; + + /// Shutter speed (sec). + final ShutterSpeedEnum? shutterSpeed; + + /// White balance. + final WhiteBalanceEnum? whiteBalance; + + BracketSetting( + this.aperture, + this.colorTemperature, + this.exposureCompensation, + this.exposureProgram, + this.iso, + this.shutterSpeed, + this.whiteBalance); + + @override + bool operator ==(Object other) => hashCode == other.hashCode; + + @override + int get hashCode => Object.hashAll([ + aperture, + colorTemperature, + exposureCompensation, + exposureProgram, + iso, + shutterSpeed, + whiteBalance + ]); +} + /// Camera control source. enum CameraControlSourceEnum { /// Operation is possible with the camera. Locks the smartphone @@ -3733,6 +3791,9 @@ class Options { /// Aperture value. ApertureEnum? aperture; + /// Multi bracket shooting setting. + List? autoBracket; + /// see [Bitrate] Bitrate? bitrate; @@ -4014,6 +4075,8 @@ class Options { return aiAutoThumbnail as T; case OptionNameEnum.aperture: return aperture as T; + case OptionNameEnum.autoBracket: + return autoBracket as T; case OptionNameEnum.bitrate: return bitrate as T; case OptionNameEnum.bluetoothPower: @@ -4138,6 +4201,9 @@ class Options { case OptionNameEnum.aperture: aperture = value; break; + case OptionNameEnum.autoBracket: + autoBracket = value; + break; case OptionNameEnum.bitrate: bitrate = value; break; diff --git a/flutter/lib/theta_client_flutter_method_channel.dart b/flutter/lib/theta_client_flutter_method_channel.dart index 3be53ddcaa..d664033618 100644 --- a/flutter/lib/theta_client_flutter_method_channel.dart +++ b/flutter/lib/theta_client_flutter_method_channel.dart @@ -8,11 +8,14 @@ import 'package:theta_client_flutter/utils/convert_utils.dart'; import 'theta_client_flutter_platform_interface.dart'; const notifyIdLivePreview = 10001; -const notifyIdTimeShiftProgress = 10002; +const notifyIdTimeShiftProgress = 10011; +const notifyIdTimeShiftStopError = 10012; const notifyIdVideoCaptureStopError = 10003; const notifyIdLimitlessIntervalCaptureStopError = 10004; -const notifyIdShotCountSpecifiedIntervalCaptureProgress = 10005; -const notifyIdShotCountSpecifiedIntervalCaptureStopError = 10006; +const notifyIdShotCountSpecifiedIntervalCaptureProgress = 10021; +const notifyIdShotCountSpecifiedIntervalCaptureStopError = 10022; +const notifyIdCompositeIntervalCaptureProgress = 10031; +const notifyIdCompositeIntervalCaptureStopError = 10032; /// An implementation of [ThetaClientFlutterPlatform] that uses method channels. class MethodChannelThetaClientFlutter extends ThetaClientFlutterPlatform { @@ -238,8 +241,8 @@ class MethodChannelThetaClientFlutter extends ThetaClientFlutterPlatform { } @override - Future startTimeShiftCapture( - void Function(double)? onProgress) async { + Future startTimeShiftCapture(void Function(double)? onProgress, + void Function(Exception exception)? onStopFailed) async { var completer = Completer(); try { enableNotifyEventReceiver(); @@ -251,12 +254,22 @@ class MethodChannelThetaClientFlutter extends ThetaClientFlutterPlatform { } }); } + if (onStopFailed != null) { + addNotify(notifyIdTimeShiftStopError, (params) { + final message = params?['message'] as String?; + if (message != null) { + onStopFailed(Exception(message)); + } + }); + } final fileUrl = await methodChannel.invokeMethod('startTimeShiftCapture'); removeNotify(notifyIdTimeShiftProgress); + removeNotify(notifyIdTimeShiftStopError); completer.complete(fileUrl); } catch (e) { removeNotify(notifyIdTimeShiftProgress); + removeNotify(notifyIdTimeShiftStopError); completer.completeError(e); } return completer.future; @@ -417,6 +430,67 @@ class MethodChannelThetaClientFlutter extends ThetaClientFlutterPlatform { .invokeMethod('stopShotCountSpecifiedIntervalCapture'); } + @override + Future getCompositeIntervalCaptureBuilder( + int shootingTimeSec) async { + return methodChannel.invokeMethod( + 'getCompositeIntervalCaptureBuilder', shootingTimeSec); + } + + @override + Future buildCompositeIntervalCapture( + Map options, int interval) async { + final params = ConvertUtils.convertCaptureParams(options); + params['_capture_interval'] = interval; + return methodChannel.invokeMethod( + 'buildCompositeIntervalCapture', params); + } + + @override + Future?> startCompositeIntervalCapture( + void Function(double)? onProgress, + void Function(Exception exception)? onStopFailed) async { + var completer = Completer?>(); + try { + enableNotifyEventReceiver(); + if (onProgress != null) { + addNotify(notifyIdCompositeIntervalCaptureProgress, (params) { + final completion = params?['completion'] as double?; + if (completion != null) { + onProgress(completion); + } + }); + } + if (onStopFailed != null) { + addNotify(notifyIdCompositeIntervalCaptureStopError, (params) { + final message = params?['message'] as String?; + if (message != null) { + onStopFailed(Exception(message)); + } + }); + } + final fileUrls = await methodChannel.invokeMethod?>('startCompositeIntervalCapture'); + removeNotify(notifyIdCompositeIntervalCaptureProgress); + removeNotify(notifyIdCompositeIntervalCaptureStopError); + if (fileUrls == null) { + completer.complete(null); + } else { + completer.complete(ConvertUtils.convertStringList(fileUrls)); + } + } catch (e) { + removeNotify(notifyIdCompositeIntervalCaptureProgress); + removeNotify(notifyIdCompositeIntervalCaptureStopError); + completer.completeError(e); + } + return completer.future; + } + + @override + Future stopCompositeIntervalCapture() async { + return methodChannel + .invokeMethod('stopCompositeIntervalCapture'); + } + @override Future getOptions(List optionNames) async { var completer = Completer(); diff --git a/flutter/lib/theta_client_flutter_platform_interface.dart b/flutter/lib/theta_client_flutter_platform_interface.dart index 6e86e30b58..c268bf8ec9 100644 --- a/flutter/lib/theta_client_flutter_platform_interface.dart +++ b/flutter/lib/theta_client_flutter_platform_interface.dart @@ -107,7 +107,8 @@ abstract class ThetaClientFlutterPlatform extends PlatformInterface { 'buildTimeShiftCapture() has not been implemented.'); } - Future startTimeShiftCapture(void Function(double)? onProgress) { + Future startTimeShiftCapture(void Function(double)? onProgress, + void Function(Exception exception)? onStopFailed) { throw UnimplementedError( 'startTimeShiftCapture() has not been implemented.'); } @@ -179,6 +180,29 @@ abstract class ThetaClientFlutterPlatform extends PlatformInterface { 'stopShotCountSpecifiedIntervalCapture() has not been implemented.'); } + Future getCompositeIntervalCaptureBuilder(int shootingTimeSec) { + throw UnimplementedError( + 'getCompositeIntervalCaptureBuilder() has not been implemented.'); + } + + Future buildCompositeIntervalCapture( + Map options, int interval) { + throw UnimplementedError( + 'buildCompositeIntervalCapture() has not been implemented.'); + } + + Future?> startCompositeIntervalCapture( + void Function(double)? onProgress, + void Function(Exception exception)? onStopFailed) { + throw UnimplementedError( + 'startCompositeIntervalCapture() has not been implemented.'); + } + + Future stopCompositeIntervalCapture() { + throw UnimplementedError( + 'stopCompositeIntervalCapture() has not been implemented.'); + } + Future getOptions(List optionNames) { throw UnimplementedError('getOptions() has not been implemented.'); } diff --git a/flutter/lib/utils/convert_utils.dart b/flutter/lib/utils/convert_utils.dart index 7e9132e7e6..a204902e20 100644 --- a/flutter/lib/utils/convert_utils.dart +++ b/flutter/lib/utils/convert_utils.dart @@ -2,6 +2,60 @@ import 'package:theta_client_flutter/digest_auth.dart'; import 'package:theta_client_flutter/theta_client_flutter.dart'; class ConvertUtils { + static List? convertAutoBracketOption(List? data) { + if (data == null) { + return null; + } + + var autoBracket = List.empty(growable: true); + + for (var element in data.cast>()) { + autoBracket.add(BracketSetting( + (element['aperture'] != null) + ? ApertureEnum.getValue(element['aperture'] as String) + : null, + element['colorTemperature'], + (element['exposureCompensation'] != null) + ? ExposureCompensationEnum.getValue( + element['exposureCompensation'] as String) + : null, + (element['exposureProgram'] != null) + ? ExposureProgramEnum.getValue( + element['exposureProgram'] as String) + : null, + (element['iso'] != null) + ? IsoEnum.getValue(element['iso'] as String) + : null, + (element['shutterSpeed'] != null) + ? ShutterSpeedEnum.getValue(element['shutterSpeed'] as String) + : null, + (element['whiteBalance'] != null) + ? WhiteBalanceEnum.getValue(element['whiteBalance'] as String) + : null)); + } + + return autoBracket; + } + + static List> convertAutoBracketOptionParam( + List bracketSetting) { + var list = List>.empty(growable: true); + + for (var bracketSetting in bracketSetting) { + list.add({ + 'aperture': bracketSetting.aperture?.rawValue, + 'colorTemperature': bracketSetting.colorTemperature, + 'exposureCompensation': bracketSetting.exposureCompensation?.rawValue, + 'exposureProgram': bracketSetting.exposureProgram?.rawValue, + 'iso': bracketSetting.iso?.rawValue, + 'shutterSpeed': bracketSetting.shutterSpeed?.rawValue, + 'whiteBalance': bracketSetting.whiteBalance?.rawValue, + }); + } + + return list; + } + static BurstOption? convertBurstOption(Map? data) { if (data == null) { return null; @@ -252,6 +306,9 @@ class ConvertUtils { case OptionNameEnum.aperture: result.aperture = ApertureEnum.getValue(entry.value); break; + case OptionNameEnum.autoBracket: + result.autoBracket = convertAutoBracketOption(entry.value); + break; case OptionNameEnum.bitrate: result.bitrate = (entry.value is int) ? BitrateNumber(entry.value) @@ -451,6 +508,8 @@ class ConvertUtils { return value.rawValue; } else if (value is BluetoothPowerEnum) { return value.rawValue; + } else if (value is List) { + return convertAutoBracketOptionParam(value); } else if (value is BurstModeEnum) { return value.rawValue; } else if (value is BurstOption) { diff --git a/flutter/test/capture/composite_interval_capture_method_channel_test.dart b/flutter/test/capture/composite_interval_capture_method_channel_test.dart new file mode 100644 index 0000000000..cfcc770591 --- /dev/null +++ b/flutter/test/capture/composite_interval_capture_method_channel_test.dart @@ -0,0 +1,163 @@ +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(() { + platform = MethodChannelThetaClientFlutter(); + channel.setMockMethodCallHandler(null); + }); + + tearDown(() { + channel.setMockMethodCallHandler(null); + }); + + test('buildCompositeIntervalCapture', () async { + Map gpsInfoMap = { + 'latitude': 1.0, + 'longitude': 2.0, + 'altitude': 3.0, + 'dateTimeZone': '2022:01:01 00:01:00+09:00' + }; + List> data = [ + ['Aperture', ApertureEnum.aperture_2_0, 'APERTURE_2_0'], + ['ColorTemperature', 2, 2], + ['ExposureCompensation', ExposureCompensationEnum.m0_3, 'M0_3'], + ['ExposureDelay', ExposureDelayEnum.delay1, 'DELAY_1'], + [ + 'ExposureProgram', + ExposureProgramEnum.aperturePriority, + 'APERTURE_PRIORITY' + ], + [ + 'GpsInfo', + GpsInfo(1.0, 2.0, 3.0, '2022:01:01 00:01:00+09:00'), + gpsInfoMap + ], + ['GpsTagRecording', GpsTagRecordingEnum.on, 'ON'], + ['Iso', IsoEnum.iso50, 'ISO_50'], + ['IsoAutoHighLimit', IsoAutoHighLimitEnum.iso200, 'ISO_200'], + ['WhiteBalance', WhiteBalanceEnum.bulbFluorescent, 'BULB_FLUORESCENT'], + ]; + + Map options = {}; + for (int i = 0; i < data.length; i++) { + options[data[i][0]] = data[i][1]; + } + + channel.setMockMethodCallHandler((MethodCall methodCall) async { + var arguments = methodCall.arguments as Map; + expect(arguments['_capture_interval'], 1); + for (int i = 0; i < data.length; i++) { + expect(arguments[data[i][0]], data[i][2], reason: data[i][0]); + } + return Future.value(); + }); + await platform.buildCompositeIntervalCapture(options, 1); + }); + + test('startCompositeIntervalCapture', () async { + const fileUrls = [ + 'http://192.168.1.1/files/150100524436344d4201375fda9dc400/100RICOH/R0013336.MP4' + ]; + channel.setMockMethodCallHandler((MethodCall methodCall) async { + return fileUrls; + }); + expect(await platform.startCompositeIntervalCapture(null, null), fileUrls); + }); + + test('startCompositeIntervalCapture no file', () async { + const fileUrls = null; + channel.setMockMethodCallHandler((MethodCall methodCall) async { + return fileUrls; + }); + expect(await platform.startCompositeIntervalCapture(null, null), null); + }); + + test('startCompositeIntervalCapture exception', () async { + channel.setMockMethodCallHandler((MethodCall methodCall) async { + throw Exception('test error'); + }); + try { + await platform.startCompositeIntervalCapture(null, null); + expect(true, false, reason: 'not exception'); + } catch (error) { + expect(error.toString().contains('test error'), true); + } + }); + + test('progress of capture', () async { + const fileUrls = [ + 'http://192.168.1.1/files/150100524436344d4201375fda9dc400/100RICOH/R0013336.MP4' + ]; + channel.setMockMethodCallHandler((MethodCall methodCall) async { + expect(platform.notifyList.containsKey(10031), true, + reason: 'add notify progress'); + + // native event + platform.onNotify({ + 'id': 10031, + 'params': { + 'completion': 0.1, + }, + }); + await Future.delayed(const Duration(milliseconds: 10)); + platform.onNotify({ + 'id': 10031, + 'params': { + 'completion': 0.2, + }, + }); + await Future.delayed(const Duration(milliseconds: 10)); + + return fileUrls; + }); + + int progressCount = 0; + var resultCapture = platform.startCompositeIntervalCapture((completion) { + progressCount++; + }, null); + var result = await resultCapture.timeout(const Duration(seconds: 5)); + expect(result, fileUrls); + expect(progressCount, 2); + expect(platform.notifyList.containsKey(10031), false, + reason: 'remove notify progress'); + }); + + test('call onStopFailed', () async { + const fileUrls = [ + 'http://192.168.1.1/files/150100524436344d4201375fda9dc400/100RICOH/R0013336.MP4' + ]; + channel.setMockMethodCallHandler((MethodCall methodCall) async { + expect(platform.notifyList.containsKey(10032), true, + reason: 'add notify stop error'); + + await Future.delayed(const Duration(milliseconds: 1)); + // native event + platform.onNotify({ + 'id': 10032, + 'params': { + 'message': "stop error", + }, + }); + return fileUrls; + }); + + var isOnStopFailed = false; + var resultCapture = + platform.startCompositeIntervalCapture(null, (exception) { + isOnStopFailed = true; + }); + var result = await resultCapture.timeout(const Duration(seconds: 5)); + expect(result, fileUrls); + expect(platform.notifyList.containsKey(10032), false, + reason: 'remove notify stop error'); + expect(isOnStopFailed, true); + }); +} diff --git a/flutter/test/capture/composite_interval_capture_test.dart b/flutter/test/capture/composite_interval_capture_test.dart new file mode 100644 index 0000000000..196aedbd67 --- /dev/null +++ b/flutter/test/capture/composite_interval_capture_test.dart @@ -0,0 +1,277 @@ +import 'dart:async'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:theta_client_flutter/theta_client_flutter.dart'; +import 'package:theta_client_flutter/theta_client_flutter_platform_interface.dart'; + +import '../theta_client_flutter_test.dart'; + +void main() { + setUp(() {}); + + tearDown(() { + onCallGetCompositeIntervalCaptureBuilder = Future.value; + onCallBuildCompositeIntervalCapture = (options, interval) => Future.value(); + }); + + test('getCompositeIntervalCaptureBuilder', () async { + ThetaClientFlutter thetaClientPlugin = ThetaClientFlutter(); + MockThetaClientFlutterPlatform fakePlatform = + MockThetaClientFlutterPlatform(); + ThetaClientFlutterPlatform.instance = fakePlatform; + + const shootingTimeSec = 600; + var builder = + thetaClientPlugin.getCompositeIntervalCaptureBuilder(shootingTimeSec); + expect(builder, isNotNull); + }); + + test('buildCompositeIntervalCapture', () async { + ThetaClientFlutter thetaClientPlugin = ThetaClientFlutter(); + MockThetaClientFlutterPlatform fakePlatform = + MockThetaClientFlutterPlatform(); + ThetaClientFlutterPlatform.instance = fakePlatform; + + const shootingTimeSec = 600; + const compositeShootingOutputInterval = 60; + + const aperture = [ApertureEnum.aperture_2_0, 'Aperture']; + const colorTemperature = [2, 'ColorTemperature']; + const exposureCompensation = [ + ExposureCompensationEnum.m0_3, + 'ExposureCompensation' + ]; + const exposureDelay = [ExposureDelayEnum.delay1, 'ExposureDelay']; + const exposureProgram = [ + ExposureProgramEnum.aperturePriority, + 'ExposureProgram' + ]; + final gpsInfo = [ + GpsInfo(1.0, 2.0, 3.0, '2022:01:01 00:01:00+09:00'), + 'GpsInfo' + ]; + const gpsTagRecording = [GpsTagRecordingEnum.on, 'GpsTagRecording']; + const iso = [IsoEnum.iso100, 'Iso']; + const isoAutoHighLimit = [IsoAutoHighLimitEnum.iso125, 'IsoAutoHighLimit']; + const whiteBalance = [WhiteBalanceEnum.auto, 'WhiteBalance']; + + onCallBuildCompositeIntervalCapture = (options, interval) { + expect(options[aperture[1]], aperture[0]); + expect(options[colorTemperature[1]], colorTemperature[0]); + expect(options[exposureCompensation[1]], exposureCompensation[0]); + expect(options[exposureDelay[1]], exposureDelay[0]); + expect(options[exposureProgram[1]], exposureProgram[0]); + expect(options[gpsInfo[1]], gpsInfo[0]); + expect(options[gpsTagRecording[1]], gpsTagRecording[0]); + expect(options[iso[1]], iso[0]); + expect(options[isoAutoHighLimit[1]], isoAutoHighLimit[0]); + expect(options[whiteBalance[1]], whiteBalance[0]); + return Future.value(null); + }; + + final builder = + thetaClientPlugin.getCompositeIntervalCaptureBuilder(shootingTimeSec); + builder.setCompositeShootingOutputInterval(compositeShootingOutputInterval); + builder.setAperture(aperture[0] as ApertureEnum); + builder.setColorTemperature(colorTemperature[0] as int); + builder.setExposureCompensation( + exposureCompensation[0] as ExposureCompensationEnum); + builder.setExposureDelay(exposureDelay[0] as ExposureDelayEnum); + builder.setExposureProgram(exposureProgram[0] as ExposureProgramEnum); + builder.setGpsInfo(gpsInfo[0] as GpsInfo); + builder.setGpsTagRecording(gpsTagRecording[0] as GpsTagRecordingEnum); + builder.setIso(iso[0] as IsoEnum); + builder.setIsoAutoHighLimit(isoAutoHighLimit[0] as IsoAutoHighLimitEnum); + builder.setWhiteBalance(whiteBalance[0] as WhiteBalanceEnum); + + var capture = await builder.build(); + expect(capture, isNotNull); + expect(capture.getAperture(), aperture[0]); + expect(capture.getColorTemperature(), colorTemperature[0]); + expect(capture.getExposureCompensation(), exposureCompensation[0]); + expect(capture.getExposureDelay(), exposureDelay[0]); + expect(capture.getExposureProgram(), exposureProgram[0]); + expect(capture.getGpsInfo(), gpsInfo[0]); + expect(capture.getGpsTagRecording(), gpsTagRecording[0]); + expect(capture.getIso(), iso[0]); + expect(capture.getIsoAutoHighLimit(), isoAutoHighLimit[0]); + expect(capture.getWhiteBalance(), whiteBalance[0]); + expect(capture.getCompositeShootingOutputInterval(), + compositeShootingOutputInterval); + }); + + test('startCompositeIntervalCapture', () async { + ThetaClientFlutter thetaClientPlugin = ThetaClientFlutter(); + MockThetaClientFlutterPlatform fakePlatform = + MockThetaClientFlutterPlatform(); + ThetaClientFlutterPlatform.instance = fakePlatform; + + const shootingTimeSec = 600; + const imageUrls = ['http://test.jpeg']; + + onCallStartCompositeIntervalCapture = (onProgress, onStopFailed) { + return Future.value(imageUrls); + }; + + var builder = + thetaClientPlugin.getCompositeIntervalCaptureBuilder(shootingTimeSec); + var capture = await builder.build(); + List? fileUrls; + capture.startCapture( + (value) { + expect(value, imageUrls); + fileUrls = value; + }, + (completion) {}, + (exception) { + expect(false, isTrue, reason: 'Error. startCapture'); + }); + + await Future.delayed(const Duration(milliseconds: 10), () {}); + expect(fileUrls, imageUrls); + expect(capture.getAperture(), isNull); + }); + + test('startCompositeIntervalCapture Exception', () async { + ThetaClientFlutter thetaClientPlugin = ThetaClientFlutter(); + MockThetaClientFlutterPlatform fakePlatform = + MockThetaClientFlutterPlatform(); + ThetaClientFlutterPlatform.instance = fakePlatform; + + var completer = Completer>(); + onCallStartCompositeIntervalCapture = (onProgress, onStopFailed) { + return completer.future; + }; + onCallStopCompositeIntervalCapture = () { + completer + .completeError(Exception('Error. startCompositeIntervalCapture')); + return Future.value(); + }; + + const shootingTimeSec = 600; + var builder = + thetaClientPlugin.getCompositeIntervalCaptureBuilder(shootingTimeSec); + var capture = await builder.build(); + var capturing = capture.startCapture( + (fileUrl) { + expect(false, isTrue, reason: 'startCapture'); + }, + (completion) {}, + (exception) { + expect(exception, isNotNull, reason: 'Error. startCapture'); + }); + + capturing.stopCapture(); + await Future.delayed(const Duration(milliseconds: 10), () {}); + }); + + test('stopCompositeIntervalCapture', () async { + ThetaClientFlutter thetaClientPlugin = ThetaClientFlutter(); + MockThetaClientFlutterPlatform fakePlatform = + MockThetaClientFlutterPlatform(); + ThetaClientFlutterPlatform.instance = fakePlatform; + + const shootingTimeSec = 600; + const imageUrls = ['http://test.jpeg']; + + var completer = Completer>(); + onCallStartCompositeIntervalCapture = (onProgress, onStopFailed) { + return completer.future; + }; + onCallStopCompositeIntervalCapture = () { + completer.complete(imageUrls); + return Future.value(); + }; + + var builder = + thetaClientPlugin.getCompositeIntervalCaptureBuilder(shootingTimeSec); + var capture = await builder.build(); + List? fileUrls; + var capturing = capture.startCapture( + (value) { + expect(value, imageUrls); + fileUrls = value; + }, + (completion) {}, + (exception) { + expect(false, isTrue, reason: 'Error. startCapture'); + }); + + capturing.stopCapture(); + await Future.delayed(const Duration(milliseconds: 10), () {}); + expect(fileUrls, imageUrls); + }); + + test('call onStopFailed', () async { + ThetaClientFlutter thetaClientPlugin = ThetaClientFlutter(); + MockThetaClientFlutterPlatform fakePlatform = + MockThetaClientFlutterPlatform(); + ThetaClientFlutterPlatform.instance = fakePlatform; + + var completer = Completer(); + + void Function(Exception exception)? paramStopFailed; + + onCallStartCompositeIntervalCapture = (onProgress, onStopFailed) { + paramStopFailed = onStopFailed; + return Completer>().future; + }; + onCallStopCompositeIntervalCapture = () { + paramStopFailed?.call(Exception("on stop error.")); + return Future.value(); + }; + + const shootingTimeSec = 600; + var builder = + thetaClientPlugin.getCompositeIntervalCaptureBuilder(shootingTimeSec); + var capture = await builder.build(); + var isOnStopFailed = false; + var capturing = capture.startCapture( + (fileUrl) { + expect(false, isTrue, reason: 'startCapture'); + }, + (completion) {}, + (exception) {}, + onStopFailed: (exception) { + expect(exception, isNotNull, reason: 'Error. stopCapture'); + isOnStopFailed = true; + completer.complete(null); + }); + + capturing.stopCapture(); + await completer.future.timeout(const Duration(milliseconds: 10)); + expect(isOnStopFailed, true); + }); + + test('call onProgress', () async { + ThetaClientFlutter thetaClientPlugin = ThetaClientFlutter(); + MockThetaClientFlutterPlatform fakePlatform = + MockThetaClientFlutterPlatform(); + ThetaClientFlutterPlatform.instance = fakePlatform; + + var completer = Completer(); + + void Function(double completion)? paramOnProgress; + + onCallStartCompositeIntervalCapture = (onProgress, onStopFailed) { + paramOnProgress = onProgress; + return Completer>().future; + }; + + const shootingTimeSec = 600; + var builder = + thetaClientPlugin.getCompositeIntervalCaptureBuilder(shootingTimeSec); + var capture = await builder.build(); + var isOnProgress = false; + capture.startCapture((fileUrl) { + expect(false, isTrue, reason: 'startCapture'); + }, (completion) { + isOnProgress = true; + completer.complete(null); + }, (exception) {}); + + paramOnProgress?.call(0.1); + await completer.future.timeout(const Duration(milliseconds: 10)); + expect(isOnProgress, true); + }); +} diff --git a/flutter/test/capture/shot_count_specified_interval_capture_method_channel_test.dart b/flutter/test/capture/shot_count_specified_interval_capture_method_channel_test.dart index a454ead2b3..461774af14 100644 --- a/flutter/test/capture/shot_count_specified_interval_capture_method_channel_test.dart +++ b/flutter/test/capture/shot_count_specified_interval_capture_method_channel_test.dart @@ -99,19 +99,19 @@ void main() { 'http://192.168.1.1/files/150100524436344d4201375fda9dc400/100RICOH/R0013336.MP4' ]; channel.setMockMethodCallHandler((MethodCall methodCall) async { - expect(platform.notifyList.containsKey(10005), true, + expect(platform.notifyList.containsKey(10021), true, reason: 'add notify progress'); // native event platform.onNotify({ - 'id': 10005, + 'id': 10021, 'params': { 'completion': 0.1, }, }); await Future.delayed(const Duration(milliseconds: 10)); platform.onNotify({ - 'id': 10005, + 'id': 10021, 'params': { 'completion': 0.2, }, @@ -129,7 +129,7 @@ void main() { var result = await resultCapture.timeout(const Duration(seconds: 5)); expect(result, fileUrls); expect(progressCount, 2); - expect(platform.notifyList.containsKey(10005), false, + expect(platform.notifyList.containsKey(10021), false, reason: 'remove notify progress'); }); @@ -138,13 +138,13 @@ void main() { 'http://192.168.1.1/files/150100524436344d4201375fda9dc400/100RICOH/R0013336.MP4' ]; channel.setMockMethodCallHandler((MethodCall methodCall) async { - expect(platform.notifyList.containsKey(10006), true, + expect(platform.notifyList.containsKey(10022), true, reason: 'add notify stop error'); await Future.delayed(const Duration(milliseconds: 1)); // native event platform.onNotify({ - 'id': 10006, + 'id': 10022, 'params': { 'message': "stop error", }, @@ -159,7 +159,7 @@ void main() { }); var result = await resultCapture.timeout(const Duration(seconds: 5)); expect(result, fileUrls); - expect(platform.notifyList.containsKey(10006), false, + expect(platform.notifyList.containsKey(10022), false, reason: 'remove notify stop error'); expect(isOnStopFailed, true); }); diff --git a/flutter/test/capture/time_shift_capture_method_channel_test.dart b/flutter/test/capture/time_shift_capture_method_channel_test.dart index 8f8317e5d4..32b37d583c 100644 --- a/flutter/test/capture/time_shift_capture_method_channel_test.dart +++ b/flutter/test/capture/time_shift_capture_method_channel_test.dart @@ -48,7 +48,7 @@ void main() { channel.setMockMethodCallHandler((MethodCall methodCall) async { return fileUrl; }); - expect(await platform.startTimeShiftCapture(null), fileUrl); + expect(await platform.startTimeShiftCapture(null, null), fileUrl); }); test('startTimeShiftCapture no file', () async { @@ -56,7 +56,7 @@ void main() { channel.setMockMethodCallHandler((MethodCall methodCall) async { return fileUrl; }); - expect(await platform.startTimeShiftCapture(null), null); + expect(await platform.startTimeShiftCapture(null, null), null); }); test('startTimeShiftCapture exception', () async { @@ -64,7 +64,7 @@ void main() { throw Exception('test error'); }); try { - await platform.startTimeShiftCapture(null); + await platform.startTimeShiftCapture(null, null); expect(true, false, reason: 'not exception'); } catch (error) { expect(error.toString().contains('test error'), true); @@ -76,19 +76,19 @@ void main() { 'http://192.168.1.1/files/150100524436344d4201375fda9dc400/100RICOH/R0013336.MP4'; channel.setMockMethodCallHandler((MethodCall methodCall) async { - expect(platform.notifyList.containsKey(10002), true, + expect(platform.notifyList.containsKey(10011), true, reason: 'add notify progress'); // native event platform.onNotify({ - 'id': 10002, + 'id': 10011, 'params': { 'completion': 0.1, }, }); await Future.delayed(const Duration(milliseconds: 10)); platform.onNotify({ - 'id': 10002, + 'id': 10011, 'params': { 'completion': 0.2, }, @@ -101,11 +101,11 @@ void main() { int progressCount = 0; var resultCapture = platform.startTimeShiftCapture((completion) { progressCount++; - }); + }, null); var result = await resultCapture.timeout(const Duration(seconds: 5)); expect(result, fileUrl); expect(progressCount, 2); - expect(platform.notifyList.containsKey(10002), false, + expect(platform.notifyList.containsKey(10011), false, reason: 'remove notify progress'); }); } diff --git a/flutter/test/capture/time_shift_capture_test.dart b/flutter/test/capture/time_shift_capture_test.dart index 933835e9ca..c3e7cca423 100644 --- a/flutter/test/capture/time_shift_capture_test.dart +++ b/flutter/test/capture/time_shift_capture_test.dart @@ -67,7 +67,7 @@ void main() { const imageUrl = 'http://test.JPG'; - onCallStartTimeShiftCapture = (onProgress) { + onCallStartTimeShiftCapture = (onProgress, onStopFailed) { return Future.value(imageUrl); }; @@ -95,7 +95,7 @@ void main() { ThetaClientFlutterPlatform.instance = fakePlatform; var completer = Completer(); - onCallStartTimeShiftCapture = (onProgress) { + onCallStartTimeShiftCapture = (onProgress, onStopFailed) { return completer.future; }; onCallStopTimeShiftCapture = () { @@ -127,7 +127,7 @@ void main() { const imageUrl = 'http://test.mp4'; var completer = Completer(); - onCallStartTimeShiftCapture = (onProgress) { + onCallStartTimeShiftCapture = (onProgress, onStopFailed) { return completer.future; }; onCallStopTimeShiftCapture = () { @@ -152,4 +152,44 @@ void main() { await Future.delayed(const Duration(milliseconds: 10), () {}); expect(fileUrl, imageUrl); }); + + test('call onStopFailed', () async { + ThetaClientFlutter thetaClientPlugin = ThetaClientFlutter(); + MockThetaClientFlutterPlatform fakePlatform = + MockThetaClientFlutterPlatform(); + ThetaClientFlutterPlatform.instance = fakePlatform; + + var completer = Completer(); + + void Function(Exception exception)? paramStopFailed; + + onCallStartTimeShiftCapture = (onProgress, onStopFailed) { + paramStopFailed = onStopFailed; + return Completer().future; + }; + onCallStopTimeShiftCapture = () { + paramStopFailed?.call(Exception("on stop error.")); + return Future.value(); + }; + + var builder = thetaClientPlugin + .getTimeShiftCaptureBuilder(); + var capture = await builder.build(); + var isOnStopFailed = false; + var capturing = capture.startCapture( + (fileUrl) { + expect(false, isTrue, reason: 'startCapture'); + }, + (completion) {}, + (exception) {}, + onStopFailed: (exception) { + expect(exception, isNotNull, reason: 'Error. stopCapture'); + isOnStopFailed = true; + completer.complete(null); + }); + + capturing.stopCapture(); + await completer.future.timeout(const Duration(milliseconds: 10)); + expect(isOnStopFailed, true); + }); } diff --git a/flutter/test/theta_client_flutter_method_channel_test.dart b/flutter/test/theta_client_flutter_method_channel_test.dart index dec74149a1..25548c731c 100644 --- a/flutter/test/theta_client_flutter_method_channel_test.dart +++ b/flutter/test/theta_client_flutter_method_channel_test.dart @@ -364,6 +364,17 @@ void main() { }); test('getOptions', () async { + List> bracketSetting = [ + { + 'aperture': 'APERTURE_2_1', + 'colorTemperature': 5000, + 'exposureCompensation': 'ZERO', + 'exposureProgram': 'MANUAL', + 'iso': 'ISO_400', + 'shutterSpeed': 'SHUTTER_SPEED_ONE_OVER_250', + 'whiteBalance': 'AUTO' + } + ]; Map gpsInfoMap = { 'latitude': 1.0, 'longitude': 2.0, @@ -400,6 +411,21 @@ void main() { ApertureEnum.aperture_2_0, 'APERTURE_2_0' ], + [ + OptionNameEnum.autoBracket, + 'AutoBracket', + [ + BracketSetting( + ApertureEnum.aperture_2_1, + 5000, + ExposureCompensationEnum.zero, + ExposureProgramEnum.manual, + IsoEnum.iso400, + ShutterSpeedEnum.shutterSpeedOneOver_250, + WhiteBalanceEnum.auto) + ], + bracketSetting + ], [OptionNameEnum.bitrate, 'Bitrate', Bitrate.fine, 'FINE'], [ OptionNameEnum.bluetoothPower, @@ -632,15 +658,26 @@ void main() { expect(options, isNotNull); expect(options.aperture, data[1][2]); - expect(options.cameraControlSource, data[5][2]); - expect(options.cameraMode, data[6][2]); - expect(options.captureMode, data[7][2]); + expect(options.cameraControlSource, data[6][2]); + expect(options.cameraMode, data[7][2]); + expect(options.captureMode, data[8][2]); for (int i = 0; i < data.length; i++) { expect(options.getValue(data[i][0]), data[i][2], reason: data[i][1]); } }); test('setOptions', () async { + List> autoBracketMap = [ + { + 'aperture': 'APERTURE_2_1', + 'colorTemperature': 5000, + 'exposureCompensation': 'ZERO', + 'exposureProgram': 'MANUAL', + 'iso': 'ISO_400', + 'shutterSpeed': 'SHUTTER_SPEED_ONE_OVER_250', + 'whiteBalance': 'AUTO' + } + ]; Map gpsInfoMap = { 'latitude': 1.0, 'longitude': 2.0, @@ -677,6 +714,21 @@ void main() { ApertureEnum.aperture_2_0, 'APERTURE_2_0' ], + [ + OptionNameEnum.autoBracket, + 'AutoBracket', + [ + BracketSetting( + ApertureEnum.aperture_2_1, + 5000, + ExposureCompensationEnum.zero, + ExposureProgramEnum.manual, + IsoEnum.iso400, + ShutterSpeedEnum.shutterSpeedOneOver_250, + WhiteBalanceEnum.auto) + ], + autoBracketMap, + ], [OptionNameEnum.bitrate, 'Bitrate', Bitrate.fine, 'FINE'], [ OptionNameEnum.bluetoothPower, diff --git a/flutter/test/theta_client_flutter_test.dart b/flutter/test/theta_client_flutter_test.dart index b88eb5221a..9476009f80 100644 --- a/flutter/test/theta_client_flutter_test.dart +++ b/flutter/test/theta_client_flutter_test.dart @@ -76,8 +76,9 @@ class MockThetaClientFlutterPlatform } @override - Future startTimeShiftCapture(void Function(double)? onProgress) { - return onCallStartTimeShiftCapture(onProgress); + Future startTimeShiftCapture(void Function(double)? onProgress, + void Function(Exception exception)? onStopFailed) { + return onCallStartTimeShiftCapture(onProgress, onStopFailed); } @override @@ -151,6 +152,29 @@ class MockThetaClientFlutterPlatform return onCallStopShotCountSpecifiedIntervalCapture(); } + @override + Future buildCompositeIntervalCapture( + Map options, int interval) { + return onCallBuildCompositeIntervalCapture(options, interval); + } + + @override + Future getCompositeIntervalCaptureBuilder(int shootingTimeSec) { + return onCallGetCompositeIntervalCaptureBuilder(shootingTimeSec); + } + + @override + Future?> startCompositeIntervalCapture( + void Function(double)? onProgress, + void Function(Exception exception)? onStopFailed) { + return onCallStartCompositeIntervalCapture(onProgress, onStopFailed); + } + + @override + Future stopCompositeIntervalCapture() { + return onCallStopCompositeIntervalCapture(); + } + @override Future getOptions(List optionNames) { return onCallGetOptions(optionNames); @@ -327,8 +351,8 @@ Future Function() onCallTakePicture = Future.value; Future Function() onCallGetTimeShiftCaptureBuilder = Future.value; Future Function(Map options, int interval) onCallBuildTimeShiftCapture = (options, interval) => Future.value(); -Future Function(void Function(double)? onProgress) - onCallStartTimeShiftCapture = (onProgress) => Future.value(); +Future Function(void Function(double)? onProgress, void Function(Exception exception)? onStopFailed) + onCallStartTimeShiftCapture = (onProgress, onStopFailed) => Future.value(); Future Function() onCallStopTimeShiftCapture = Future.value; Future Function() onCallGetVideoCaptureBuilder = Future.value; Future Function(Map options) onCallBuildVideoCapture = @@ -353,6 +377,15 @@ Future?> Function(void Function(double)? onProgress, (onProgress, onStopFailed) => Future.value(); Future Function() onCallStopShotCountSpecifiedIntervalCapture = Future.value; +Future Function(int shootingTimeSec) + 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) + onCallStartCompositeIntervalCapture = + (onProgress, onStopFailed) => Future.value(); +Future Function() onCallStopCompositeIntervalCapture = Future.value; Future Function(List optionNames) onCallGetOptions = (optionNames) => Future.value(Options()); Future Function(Options options) onCallSetOptions = Future.value; diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 4ab7fc6195..37d58d8b8c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Tue Sep 06 11:47:29 JST 2022 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/kotlin-multiplatform/build.gradle.kts b/kotlin-multiplatform/build.gradle.kts index bc47e246cf..200364d805 100644 --- a/kotlin-multiplatform/build.gradle.kts +++ b/kotlin-multiplatform/build.gradle.kts @@ -1,37 +1,46 @@ - import org.jetbrains.dokka.versioning.VersioningConfiguration import org.jetbrains.dokka.versioning.VersioningPlugin import java.util.Properties plugins { kotlin("multiplatform") - kotlin("plugin.serialization") version "1.7.20" + kotlin("plugin.serialization") version "1.9.10" id("com.android.library") id("maven-publish") - id("org.jetbrains.dokka") + id("org.jetbrains.dokka") version "1.9.10" kotlin("native.cocoapods") signing - id("io.gitlab.arturbosch.detekt").version("1.19.0") + id("io.gitlab.arturbosch.detekt").version("1.23.3") } dependencies { - dokkaPlugin("org.jetbrains.dokka:versioning-plugin:1.8.20") + dokkaPlugin("org.jetbrains.dokka:versioning-plugin:1.9.10") } -val theta_client_version = "1.5.0" +val thetaClientVersion = "1.5.0" +group = "com.ricoh360.thetaclient" +version = thetaClientVersion + // Init publish property initProp() kotlin { - android() + androidTarget { + compilations.all { + kotlinOptions { + jvmTarget = "1.8" + } + } + publishLibraryVariants("release") + } cocoapods { summary = "THETA Client" homepage = "https://github.com/ricohapi/theta-client" name = "THETAClient" authors = "Ricoh Co, Ltd." - version = theta_client_version - source = "{ :http => 'https://github.com/ricohapi/theta-client/releases/download/${theta_client_version}/THETAClient.xcframework.zip' }" + version = thetaClientVersion + source = "{ :http => 'https://github.com/ricohapi/theta-client/releases/download/${thetaClientVersion}/THETAClient.xcframework.zip' }" license = "MIT" ios.deploymentTarget = "14.0" framework { @@ -45,34 +54,33 @@ kotlin { iosSimulatorArm64() sourceSets { - val coroutines_version = "1.6.4" - val coroutines_mtversion = "1.6.4-native-mt" - val ktor_version = "2.1.2" + val coroutinesVersion = "1.6.4" + val ktorVersion = "2.1.2" val kryptoVersion = "3.4.0" val commonMain by getting { dependencies { // Works as common dependency as well as the platform one - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_mtversion") - implementation("org.jetbrains.kotlinx:kotlinx-io-core:0.2.1") - api("io.ktor:ktor-client-core:$ktor_version") // Applications need to use ByteReadPacket class - implementation("io.ktor:ktor-client-content-negotiation:$ktor_version") - implementation("io.ktor:ktor-client-cio:$ktor_version") - implementation("io.ktor:ktor-client-logging:$ktor_version") - implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor_version") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion") + implementation("org.jetbrains.kotlinx:kotlinx-io-core:0.3.0") + api("io.ktor:ktor-client-core:$ktorVersion") // Applications need to use ByteReadPacket class + implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion") + implementation("io.ktor:ktor-client-cio:$ktorVersion") + implementation("io.ktor:ktor-client-logging:$ktorVersion") + implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion") implementation("com.soywiz.korlibs.krypto:krypto:$kryptoVersion") } } val commonTest by getting { dependencies { implementation(kotlin("test")) - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version") - implementation("io.ktor:ktor-client-mock:$ktor_version") - implementation("com.goncalossilva:resources:0.2.2") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion") + implementation("io.ktor:ktor-client-mock:$ktorVersion") + implementation("com.goncalossilva:resources:0.4.0") } } val androidMain by getting - val androidTest by getting + val androidUnitTest by getting val iosX64Main by getting val iosArm64Main by getting val iosSimulatorArm64Main by getting @@ -95,11 +103,11 @@ kotlin { } android { - compileSdk = 32 + namespace = "com.ricoh360.thetaclient" + compileSdk = 33 sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") defaultConfig { minSdk = 26 - targetSdk = 32 setProperty("archivesBaseName", "theta-client") } } @@ -114,44 +122,38 @@ val javadocJar by tasks.registering(Jar::class) { afterEvaluate { initProp() publishing { - publications { - // Creates a Maven publication called "release". - create("release") { - // Applies the component for the release build variant. - from(components["release"]) - artifact(javadocJar.get()) - groupId = "com.ricoh360.thetaclient" - artifactId = "theta-client" - version = theta_client_version - pom { - name.set("theta-client") - description.set("This library provides a way to control RICOH THETA using RICOH THETA API v2.1") - url.set("https://github.com/ricohapi/theta-client") - licenses { - license { - name.set("MIT") - url.set("https://github.com/ricohapi/theta-client/blob/main/LICENSE") - } - } - developers { - developer { - organization.set("RICOH360") - organizationUrl.set("https://github.com/ricohapi/theta-client") - } + publications.withType(MavenPublication::class) { + artifact(javadocJar.get()) + when (name) { + "androidRelease" -> { + artifactId = "theta-client" + } + + else -> { + artifactId = "theta-client-$name" + } + } + pom { + name.set("theta-client") + description.set("This library provides a way to control RICOH THETA using RICOH THETA API v2.1") + url.set("https://github.com/ricohapi/theta-client") + licenses { + license { + name.set("MIT") + url.set("https://github.com/ricohapi/theta-client/blob/main/LICENSE") } - scm { - connection.set("scm:git:git@github.com:ricohapi/theta-client.git") - developerConnection.set("scm:git:git@github.com:ricohapi/theta-client.git") - url.set("https://github.com/ricohapi/theta-client/tree/main") + } + developers { + developer { + organization.set("RICOH360") + organizationUrl.set("https://github.com/ricohapi/theta-client") } } - } - create("debug") { - // Applies the component for the debug build variant. - from(components["debug"]) - groupId = "com.ricoh360.thetaclient" - artifactId = "theta-client-debug" - version = theta_client_version + scm { + connection.set("scm:git:git@github.com:ricohapi/theta-client.git") + developerConnection.set("scm:git:git@github.com:ricohapi/theta-client.git") + url.set("https://github.com/ricohapi/theta-client/tree/main") + } } } repositories { @@ -181,7 +183,7 @@ detekt { ignoreFailures = false buildUponDefaultConfig = true // preconfigure defaults allRules = false // activate all available (even unstable) rules. - config = files("$rootDir/config/detekt.yml") // config file + config.setFrom("$rootDir/config/detekt.yml") // config file baseline = file("$rootDir/config/baseline.xml") source = files( "$rootDir/kotlin-multiplatform/src/commonMain/", @@ -225,12 +227,12 @@ fun getExtraString(name: String): String? { tasks.dokkaHtml.configure { moduleName.set("theta-client") - if(project.properties["version"].toString() != theta_client_version) { + if (project.properties["version"].toString() != thetaClientVersion) { throw GradleException("The release version does not match the version defined in Gradle.") } val pagesDir = file(project.properties["workspace"].toString()).resolve("gh-pages") - val currentVersion = theta_client_version + val currentVersion = thetaClientVersion val currentDocsDir = pagesDir.resolve("docs") val docVersionsDir = pagesDir.resolve("version") outputDirectory.set(currentDocsDir) diff --git a/kotlin-multiplatform/src/androidMain/AndroidManifest.xml b/kotlin-multiplatform/src/androidMain/AndroidManifest.xml index 153bf5d1dc..740532cfa8 100644 --- a/kotlin-multiplatform/src/androidMain/AndroidManifest.xml +++ b/kotlin-multiplatform/src/androidMain/AndroidManifest.xml @@ -1,2 +1,2 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/kotlin-multiplatform/src/androidTest/kotlin/com/ricoh360/thetaclient/UpdateFirmwareOnActualTheta.kt b/kotlin-multiplatform/src/androidUnitTest/kotlin/com/ricoh360/thetaclient/UpdateFirmwareOnActualTheta.kt similarity index 100% rename from kotlin-multiplatform/src/androidTest/kotlin/com/ricoh360/thetaclient/UpdateFirmwareOnActualTheta.kt rename to kotlin-multiplatform/src/androidUnitTest/kotlin/com/ricoh360/thetaclient/UpdateFirmwareOnActualTheta.kt diff --git a/kotlin-multiplatform/src/androidTest/kotlin/com/ricoh360/thetaclient/androidTest.kt b/kotlin-multiplatform/src/androidUnitTest/kotlin/com/ricoh360/thetaclient/androidTest.kt similarity index 100% rename from kotlin-multiplatform/src/androidTest/kotlin/com/ricoh360/thetaclient/androidTest.kt rename to kotlin-multiplatform/src/androidUnitTest/kotlin/com/ricoh360/thetaclient/androidTest.kt 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 c5146e743a..5f7471b0e7 100644 --- a/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/ThetaRepository.kt +++ b/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/ThetaRepository.kt @@ -1,10 +1,6 @@ package com.ricoh360.thetaclient -import com.ricoh360.thetaclient.capture.LimitlessIntervalCapture -import com.ricoh360.thetaclient.capture.PhotoCapture -import com.ricoh360.thetaclient.capture.ShotCountSpecifiedIntervalCapture -import com.ricoh360.thetaclient.capture.TimeShiftCapture -import com.ricoh360.thetaclient.capture.VideoCapture +import com.ricoh360.thetaclient.capture.* import com.ricoh360.thetaclient.transferred.* import io.ktor.client.call.* import io.ktor.client.plugins.* @@ -672,6 +668,12 @@ class ThetaRepository internal constructor(val endpoint: String, config: Config? */ Aperture("aperture", ApertureEnum::class), + /** + * Option name + * _autoBracket + */ + AutoBracket("_autoBracket", BracketSettingList::class), + /** * Option name * _bitrate @@ -1023,6 +1025,11 @@ class ThetaRepository internal constructor(val endpoint: String, config: Config? */ var aperture: ApertureEnum? = null, + /** + * Multi bracket shooting setting. + */ + var autoBracket: BracketSettingList? = null, + /** * @see Bitrate */ @@ -1403,6 +1410,7 @@ class ThetaRepository internal constructor(val endpoint: String, config: Config? constructor() : this( aiAutoThumbnail = null, aperture = null, + autoBracket = null, bitrate = null, bluetoothPower = null, burstMode = null, @@ -1462,6 +1470,7 @@ class ThetaRepository internal constructor(val endpoint: String, config: Config? internal constructor(options: com.ricoh360.thetaclient.transferred.Options) : this( aiAutoThumbnail = options._aiAutoThumbnail?.let { AiAutoThumbnailEnum.get(it) }, aperture = options.aperture?.let { ApertureEnum.get(it) }, + autoBracket = options._autoBracket?.let { BracketSettingList.get(it) }, bitrate = options._bitrate?.let { BitrateEnum.get(it) }, bluetoothPower = options._bluetoothPower?.let { BluetoothPowerEnum.get(it) }, burstMode = options._burstMode?.let { BurstModeEnum.get(it) }, @@ -1530,6 +1539,7 @@ class ThetaRepository internal constructor(val endpoint: String, config: Config? return Options( _aiAutoThumbnail = aiAutoThumbnail?.value, aperture = aperture?.value, + _autoBracket = autoBracket?.toTransferredAutoBracket(), _bitrate = bitrate?.rawValue, _bluetoothPower = bluetoothPower?.value, _burstMode = burstMode?.value, @@ -1601,6 +1611,7 @@ class ThetaRepository internal constructor(val endpoint: String, config: Config? return when (name) { OptionNameEnum.AiAutoThumbnail -> aiAutoThumbnail OptionNameEnum.Aperture -> aperture + OptionNameEnum.AutoBracket -> autoBracket OptionNameEnum.Bitrate -> bitrate OptionNameEnum.BluetoothPower -> bluetoothPower OptionNameEnum.BurstMode -> burstMode @@ -1673,6 +1684,7 @@ class ThetaRepository internal constructor(val endpoint: String, config: Config? when (name) { OptionNameEnum.AiAutoThumbnail -> aiAutoThumbnail = value as AiAutoThumbnailEnum OptionNameEnum.Aperture -> aperture = value as ApertureEnum + OptionNameEnum.AutoBracket -> autoBracket = value as BracketSettingList OptionNameEnum.Bitrate -> bitrate = value as Bitrate OptionNameEnum.BluetoothPower -> bluetoothPower = value as BluetoothPowerEnum OptionNameEnum.BurstMode -> burstMode = value as BurstModeEnum @@ -1821,6 +1833,108 @@ class ThetaRepository internal constructor(val endpoint: String, config: Config? } } + /** + * List of [BracketSetting] used for multi bracket shooting. + * Size of the list must be 2 to 13 (THETA X and SC2), or 2 to 19 (THETA Z1 and V). + */ + data class BracketSettingList(val list: MutableList = mutableListOf()) { + fun add(setting: BracketSetting): BracketSettingList { + list.add(setting) + return this + } + + companion object { + /** + * Convert AutoBracket to BracketSettingList + */ + internal fun get(autoBracket: AutoBracket): BracketSettingList { + val list = BracketSettingList() + autoBracket._bracketParameters.forEach { param -> + val setting = BracketSetting( + aperture = param.aperture?.let { ApertureEnum.get(it) }, + colorTemperature = param._colorTemperature, + exposureCompensation = param.exposureCompensation?.let { ExposureCompensationEnum.get(it) }, + exposureProgram = param.exposureProgram?.let { ExposureProgramEnum.get(it) }, + iso = param.iso?.let { IsoEnum.get(it) }, + shutterSpeed = param.shutterSpeed?.let { ShutterSpeedEnum.get(it) }, + whiteBalance = param.whiteBalance?.let { WhiteBalanceEnum.get(it) } + ) + list.add(setting) + } + return list + } + } + + /** + * Generate transferred AutoBracket instance. + * + * @return com.ricoh360.thetaclient.transferred.[AutoBracket] + */ + internal fun toTransferredAutoBracket(): AutoBracket { + val bracketParamList: MutableList = mutableListOf() + list.forEach { + val bracketParam = BracketParameter( + aperture = it.aperture?.value, + _colorTemperature = it.colorTemperature, + exposureCompensation = it.exposureCompensation?.value, + exposureProgram = it.exposureProgram?.value, + iso = it.iso?.value, + shutterSpeed = it.shutterSpeed?.value, + whiteBalance = it.whiteBalance?.value + ) + bracketParamList.add(bracketParam) + } + return AutoBracket(list.size, bracketParamList) + } + } + + /** + * Parameters for multi bracket shooting. + */ + data class BracketSetting ( + /** + * Aperture value. + * Theta X and SC2 do not support. + */ + var aperture: ApertureEnum? = null, + + /** + * Color temperature of the camera (Kelvin). + * Support value is 2500 to 10000 in 100-Kelvin units. + */ + var colorTemperature: Int? = null, + + /** + * Exposure compensation. + * Theta SC2 does not support. + */ + var exposureCompensation: ExposureCompensationEnum? = null, + + /** + * Exposure program. The exposure settings that take priority can be selected. + * Mandatory to Theta Z1 and V. + * Theta SC2 does not support. + */ + var exposureProgram: ExposureProgramEnum? = null, + + /** + * ISO sensitivity. + */ + var iso: IsoEnum? = null, + + /** + * Shutter speed. + */ + var shutterSpeed: ShutterSpeedEnum? = null, + + /** + * White balance. + * Mandatory to Theta Z1 and V. + * Theta SC2 does not support. + */ + var whiteBalance: WhiteBalanceEnum? = null, + ) + /** * Movie bit rate. * @@ -5593,9 +5707,9 @@ class ThetaRepository internal constructor(val endpoint: String, config: Config? } /** - * Get PhotoCapture.Builder for capture video. + * Get VideoCapture.Builder for capture video. * - * @return PhotoCapture.Builder + * @return VideoCapture.Builder */ fun getVideoCaptureBuilder(): VideoCapture.Builder { return VideoCapture.Builder(endpoint) @@ -5613,7 +5727,7 @@ class ThetaRepository internal constructor(val endpoint: String, config: Config? /** * Get LimitlessIntervalCapture.Builder for capture video. * - * @return PhotoCapture.Builder + * @return LimitlessIntervalCapture.Builder */ fun getLimitlessIntervalCaptureBuilder(): LimitlessIntervalCapture.Builder { return LimitlessIntervalCapture.Builder(endpoint, cameraModel) @@ -5628,6 +5742,15 @@ class ThetaRepository internal constructor(val endpoint: String, config: Config? return ShotCountSpecifiedIntervalCapture.Builder(shotCount, endpoint, cameraModel) } + /** + * Get CompositeIntervalCapture.Builder for interval composite shooting. + * + * @return CompositeIntervalCapture.Builder + */ + fun getCompositeIntervalCaptureBuilder(shootingTimeSec: Int): CompositeIntervalCapture.Builder { + return CompositeIntervalCapture.Builder(shootingTimeSec, endpoint) + } + /** * Base exception of ThetaRepository */ diff --git a/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/capture/CompositeIntervalCapture.kt b/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/capture/CompositeIntervalCapture.kt new file mode 100644 index 0000000000..396fe1a0af --- /dev/null +++ b/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/capture/CompositeIntervalCapture.kt @@ -0,0 +1,211 @@ +package com.ricoh360.thetaclient.capture + +import com.ricoh360.thetaclient.CHECK_COMMAND_STATUS_INTERVAL +import com.ricoh360.thetaclient.ThetaApi +import com.ricoh360.thetaclient.ThetaRepository +import com.ricoh360.thetaclient.transferred.* +import io.ktor.client.plugins.* +import io.ktor.serialization.* +import kotlinx.coroutines.* + +/* + * CompositeIntervalCapture + * + * @property endpoint URL of Theta web API endpoint + * @property options option of interval composite shooting + * @property checkCommandStatusInterval the interval for executing commands/status API when state "inProgress" + */ +class CompositeIntervalCapture private constructor( + private val endpoint: String, + options: Options, + private val checkStatusCommandInterval: Long +) : Capture(options) { + + private val scope = CoroutineScope(Dispatchers.Default) + + fun getCheckStatusCommandInterval(): Long { + return checkStatusCommandInterval + } + + /** + * Get In-progress save interval for interval composite shooting (sec). + */ + fun getCompositeShootingOutputInterval() = options._compositeShootingOutputInterval + + /** + * Get Shooting time for interval composite shooting (sec). + */ + fun getCompositeShootingTime() = options._compositeShootingTime + + // TODO: Add get photo option property + + /** + * Callback of startCapture + */ + interface StartCaptureCallback { + /** + * Called when state "inProgress". + * + * @param completion Progress rate of command executed + */ + fun onProgress(completion: Float) + + /** + * Called when stopCapture error occurs. + * + * @param exception Exception of error occurs + */ + fun onStopFailed(exception: ThetaRepository.ThetaRepositoryException) + + /** + * Called when error occurs. + * + * @param exception Exception of error occurs + */ + fun onCaptureFailed(exception: ThetaRepository.ThetaRepositoryException) + + /** + * Called when successful. + * + * @param fileUrls URLs of the limitless interval capture + */ + fun onCaptureCompleted(fileUrls: List?) + } + + private suspend fun monitorCommandStatus(id: String, callback: StartCaptureCallback) { + try { + var response: CommandApiResponse? = null + var state = CommandState.IN_PROGRESS + while (state == CommandState.IN_PROGRESS) { + delay(timeMillis = checkStatusCommandInterval) + response = ThetaApi.callStatusApi( + endpoint = endpoint, + params = StatusApiParams(id = id) + ) + callback.onProgress(completion = response.progress?.completion ?: 0f) + state = response.state + } + + if (response?.state == CommandState.DONE) { + val captureResponse = response as StartCaptureResponse + callback.onCaptureCompleted(fileUrls = captureResponse.results?.fileUrls ?: listOf()) + return + } + + response?.error?.let { error -> + if (error.isCanceledShootingCode()) { + callback.onCaptureCompleted(fileUrls = null) // canceled + } else { + callback.onCaptureFailed(exception = ThetaRepository.ThetaWebApiException(message = error.message)) + } + } ?: run { + callback.onCaptureCompleted(fileUrls = null) // canceled + } + } catch (e: JsonConvertException) { + callback.onCaptureFailed(exception = ThetaRepository.ThetaWebApiException(message = e.message ?: e.toString())) + } catch (e: ResponseException) { + callback.onCaptureFailed(exception = ThetaRepository.ThetaWebApiException.create(exception = e)) + } catch (e: Exception) { + callback.onCaptureFailed(exception = ThetaRepository.NotConnectedException(message = e.message ?: e.toString())) + } + } + + /** + * Starts interval composite shooting. + * + * @param callback Success or failure of the call + * @return CompositeIntervalCapturing instance + */ + fun startCapture(callback: StartCaptureCallback): CompositeIntervalCapturing { + scope.launch { + lateinit var startCaptureResponse: StartCaptureResponse + try { + val params = StartCaptureParams(_mode = ShootingMode.INTERVAL_COMPOSITE_SHOOTING) + + startCaptureResponse = ThetaApi.callStartCaptureCommand( + endpoint = endpoint, + params = params + ) + startCaptureResponse.error?.let { error -> + callback.onCaptureFailed(exception = ThetaRepository.ThetaWebApiException(message = error.message)) + return@launch + } + + delay(timeMillis = checkStatusCommandInterval) + startCaptureResponse.id?.let { + monitorCommandStatus(it, callback) + } + } catch (e: JsonConvertException) { + callback.onCaptureFailed(exception = ThetaRepository.ThetaWebApiException(message = e.message ?: e.toString())) + } catch (e: ResponseException) { + callback.onCaptureFailed(exception = ThetaRepository.ThetaWebApiException.create(exception = e)) + } catch (e: Exception) { + callback.onCaptureFailed(exception = ThetaRepository.NotConnectedException(message = e.message ?: e.toString())) + } + } + return CompositeIntervalCapturing(endpoint = endpoint, callback = callback) + } + + /* + * Builder of CompositeIntervalCapture + * + * @property shootingTimeSec Shooting time for interval composite shooting (sec) + * @property endpoint URL of Theta web API endpoint + */ + class Builder internal constructor(private val shootingTimeSec: Int, private val endpoint: String) : Capture.Builder() { + private var interval: Long? = null + + /** + * Builds an instance of a CompositeIntervalCapture that has all the combined parameters of the Options that have been added to the Builder. + * + * @return CompositeIntervalCapture + */ + @Throws(Throwable::class) + suspend fun build(): CompositeIntervalCapture { + try { + ThetaApi.callSetOptionsCommand( + endpoint = endpoint, + params = SetOptionsParams(options = Options(captureMode = CaptureMode.IMAGE)) + ).error?.let { + throw ThetaRepository.ThetaWebApiException(message = it.message) + } + + options._compositeShootingTime = shootingTimeSec + + ThetaApi.callSetOptionsCommand(endpoint = endpoint, params = SetOptionsParams(options)).error?.let { + throw ThetaRepository.ThetaWebApiException(message = it.message) + } + } catch (e: JsonConvertException) { + throw ThetaRepository.ThetaWebApiException(message = e.message ?: e.toString()) + } catch (e: ResponseException) { + throw ThetaRepository.ThetaWebApiException.create(exception = e) + } catch (e: ThetaRepository.ThetaWebApiException) { + throw e + } catch (e: Exception) { + throw ThetaRepository.NotConnectedException(message = e.message ?: e.toString()) + } + return CompositeIntervalCapture( + endpoint = endpoint, + options = options, + checkStatusCommandInterval = interval ?: CHECK_COMMAND_STATUS_INTERVAL + ) + } + + fun setCheckStatusCommandInterval(timeMillis: Long): Builder { + this.interval = timeMillis + return this + } + + /** + * Set In-progress save interval for interval composite shooting (sec). + * @param sec sec + * @return Builder + */ + fun setCompositeShootingOutputInterval(sec: Int): Builder { + options._compositeShootingOutputInterval = sec + return this + } + + // TODO: Add set photo option property + } +} diff --git a/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/capture/CompositeIntervalCapturing.kt b/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/capture/CompositeIntervalCapturing.kt new file mode 100644 index 0000000000..a1b06f4f9b --- /dev/null +++ b/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/capture/CompositeIntervalCapturing.kt @@ -0,0 +1,54 @@ +package com.ricoh360.thetaclient.capture + +import com.ricoh360.thetaclient.ThetaApi +import com.ricoh360.thetaclient.ThetaRepository +import com.ricoh360.thetaclient.transferred.StopCaptureResponse +import io.ktor.client.plugins.ResponseException +import io.ktor.serialization.JsonConvertException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +/* + * CompositeIntervalCapturing + * + * @property endpoint URL of Theta web API endpoint + * @property callback Success or failure of the call + */ +class CompositeIntervalCapturing internal constructor( + private val endpoint: String, + private val callback: CompositeIntervalCapture.StartCaptureCallback +) : Capturing() { + + private val scope = CoroutineScope(Dispatchers.Default) + + fun cancelCapture() { + stopCapture() + } + + /** + * Stops interval shooting with the shot count specified + * When call stopCapture() then call property callback. + */ + override fun stopCapture() { + scope.launch { + lateinit var response: StopCaptureResponse + try { + response = ThetaApi.callStopCaptureCommand(endpoint = endpoint) + response.error?.let { + callback.onStopFailed(exception = ThetaRepository.ThetaWebApiException(message = it.message)) + return@launch + } + } catch (e: JsonConvertException) { + callback.onStopFailed(exception = ThetaRepository.ThetaWebApiException(message = e.message ?: e.toString())) + return@launch + } catch (e: ResponseException) { + callback.onStopFailed(exception = ThetaRepository.ThetaWebApiException.create(exception = e)) + return@launch + } catch (e: Exception) { + callback.onStopFailed(exception = ThetaRepository.NotConnectedException(message = e.message ?: e.toString())) + return@launch + } + } + } +} diff --git a/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/capture/TimeShiftCapture.kt b/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/capture/TimeShiftCapture.kt index 865ff4d00c..c85c638477 100644 --- a/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/capture/TimeShiftCapture.kt +++ b/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/capture/TimeShiftCapture.kt @@ -41,25 +41,32 @@ class TimeShiftCapture private constructor( */ interface StartCaptureCallback { /** - * Called when successful. + * Called when state "inProgress". * - * @param fileUrl URL of the time-shift. When the time-shift is canceled, this URL will be null. + * @param completion Progress rate of command executed */ - fun onSuccess(fileUrl: String?) + fun onProgress(completion: Float) /** - * Called when state "inProgress". + * Called when stopCapture error occurs. * - * @param completion Progress rate of command executed + * @param exception Exception of error occurs */ - fun onProgress(completion: Float) + fun onStopFailed(exception: ThetaRepository.ThetaRepositoryException) /** * Called when error occurs. * * @param exception Exception of error occurs */ - fun onError(exception: ThetaRepository.ThetaRepositoryException) + fun onCaptureFailed(exception: ThetaRepository.ThetaRepositoryException) + + /** + * Called when successful. + * + * @param fileUrl URL of the time-shift. When the time-shift is canceled, this URL will be null. + */ + fun onCaptureCompleted(fileUrl: String?) } /** @@ -109,36 +116,38 @@ class TimeShiftCapture private constructor( "camera.takePicture" -> (response as TakePictureResponse).results?.fileUrl else -> null } - callback.onSuccess(fileUrl = fileUrl) + callback.onCaptureCompleted(fileUrl = fileUrl) return@runBlocking } val error = response.error if (error != null && !error.isCanceledShootingCode()) { - callback.onError(exception = ThetaRepository.ThetaWebApiException(message = error.message)) + callback.onCaptureFailed(exception = ThetaRepository.ThetaWebApiException(message = error.message)) + } else if (response.name == "unknown") { + callback.onCaptureFailed(exception = ThetaRepository.ThetaWebApiException(message = "Unknown response")) } else { println("timeShift canceled") - callback.onSuccess(fileUrl = null) // canceled + callback.onCaptureCompleted(fileUrl = null) // canceled } } } catch (e: JsonConvertException) { - callback.onError( + callback.onCaptureFailed( exception = ThetaRepository.ThetaWebApiException( message = e.message ?: e.toString() ) ) } catch (e: ResponseException) { if (isCanceledShootingResponse(e.response)) { - callback.onSuccess(fileUrl = null) // canceled + callback.onCaptureCompleted(fileUrl = null) // canceled } else { - callback.onError( + callback.onCaptureFailed( exception = ThetaRepository.ThetaWebApiException.create( exception = e ) ) } } catch (e: Exception) { - callback.onError( + callback.onCaptureFailed( exception = ThetaRepository.NotConnectedException( message = e.message ?: e.toString() ) diff --git a/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/capture/TimeShiftCapturing.kt b/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/capture/TimeShiftCapturing.kt index 9a5dedc595..3a9c3d7ed3 100644 --- a/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/capture/TimeShiftCapturing.kt +++ b/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/capture/TimeShiftCapturing.kt @@ -36,17 +36,17 @@ class TimeShiftCapturing internal constructor( try { response = ThetaApi.callStopCaptureCommand(endpoint = endpoint) response.error?.let { - callback.onError(exception = ThetaRepository.ThetaWebApiException(message = it.message)) + callback.onStopFailed(exception = ThetaRepository.ThetaWebApiException(message = it.message)) return@launch } } catch (e: JsonConvertException) { - callback.onError(exception = ThetaRepository.ThetaWebApiException(message = e.message ?: e.toString())) + callback.onStopFailed(exception = ThetaRepository.ThetaWebApiException(message = e.message ?: e.toString())) return@launch } catch (e: ResponseException) { - callback.onError(exception = ThetaRepository.ThetaWebApiException.create(exception = e)) + callback.onStopFailed(exception = ThetaRepository.ThetaWebApiException.create(exception = e)) return@launch } catch (e: Exception) { - callback.onError(exception = ThetaRepository.NotConnectedException(message = e.message ?: e.toString())) + callback.onStopFailed(exception = ThetaRepository.NotConnectedException(message = e.message ?: e.toString())) return@launch } } 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 8059b2b2fc..df7f4fd2e7 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 @@ -118,6 +118,16 @@ internal data class Options( */ var apertureSupport: List? = null, + /** + * Multi bracket shooting setting + */ + var _autoBracket: AutoBracket? = null, + + /** + * Supported AutoBracket. + */ + var _autoBracketSupport: AutoBracket? = null, + /** * Movie bit rate. */ @@ -782,6 +792,81 @@ internal data class Options( var _wlanFrequencySupport: List? = null, ) +/** + * Multi bracket shooting setting + * + * [_bracketNumber] is the only supported value that can be acquired by camera.getOptions. + * For [_bracketParameters], all parameters must be specified. + */ +@Serializable +internal data class AutoBracket ( + /** + * Number of shots in multi bracket shooting. + * 2 to 13 (THETA X and SC2); + * 2 to 19 (THETA Z1 and V). + */ + val _bracketNumber: Int, + + /** + * Parameter array specified for multi bracket shooting. + */ + val _bracketParameters: List, +) + +/** + * Parameter array specified for multi bracket shooting + */ +@Serializable +internal data class BracketParameter ( + /** + * Aperture value. + * Theta X and SC2 do not support. + */ + var aperture: Float? = null, + + /** + * Color temperature of the camera (Kelvin). + * 2500 to 10000. In 100-Kelvin units. + */ + @Serializable(with = NumberAsIntSerializer::class) + var _colorTemperature: Int? = null, + + /** + * Exposure compensation (EV). + * Theta SC2 does not support. + */ + var exposureCompensation: Float? = null, + + /** + * Exposure program. + * 1: Manual program, 2: Normal program, 3: Aperture priority program, + * 4: Shutter priority program, 9: ISO priority program. + * + * Mandatory to Theta Z1 and V. + * Theta SC2 does not support. + */ + @Serializable(with = NumberAsIntSerializer::class) + var exposureProgram: Int? = null, + + /** + * ISO sensitivity. + */ + @Serializable(with = NumberAsIntSerializer::class) + var iso: Int? = null, + + /** + * Shutter speed (sec). + */ + var shutterSpeed: Double? = null, + + /** + * White balance. + * Mandatory to Theta Z1 and V. + * Theta SC2 does not support. + */ + var whiteBalance: WhiteBalance? = null, +) + /** * bluetooth power */ 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 53800ed2a7..931b2881c7 100644 --- a/kotlin-multiplatform/src/commonTest/kotlin/com/ricoh360/thetaclient/CheckRequest.kt +++ b/kotlin-multiplatform/src/commonTest/kotlin/com/ricoh360/thetaclient/CheckRequest.kt @@ -59,6 +59,7 @@ internal class CheckRequest { request: HttpRequestData, aiAutoThumbnail: AiAutoThumbnail? = null, aperture: Float? = null, + autoBracket: AutoBracket? = null, bitrate: String? = null, bluetoothPower: BluetoothPower? = null, burstMode: BurstMode? = null, @@ -123,6 +124,9 @@ internal class CheckRequest { aperture?.let { assertEquals(optionsRequest.parameters.options.aperture, it, "setOptions aperture") } + autoBracket?.let { + assertEquals(optionsRequest.parameters.options._autoBracket, it, "setOptions autoBracket") + } bitrate?.let { assertEquals(optionsRequest.parameters.options._bitrate, it, "setOptions bitrate") } diff --git a/kotlin-multiplatform/src/commonTest/kotlin/com/ricoh360/thetaclient/capture/CompositeIntervalCaptureTest.kt b/kotlin-multiplatform/src/commonTest/kotlin/com/ricoh360/thetaclient/capture/CompositeIntervalCaptureTest.kt new file mode 100644 index 0000000000..a5066097d6 --- /dev/null +++ b/kotlin-multiplatform/src/commonTest/kotlin/com/ricoh360/thetaclient/capture/CompositeIntervalCaptureTest.kt @@ -0,0 +1,817 @@ +package com.ricoh360.thetaclient.capture + +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.CaptureMode +import io.ktor.client.network.sockets.* +import io.ktor.client.request.* +import io.ktor.http.* +import io.ktor.http.content.* +import io.ktor.utils.io.* +import kotlinx.coroutines.* +import kotlinx.coroutines.test.runTest +import kotlin.test.* + +@OptIn(ExperimentalCoroutinesApi::class) +class CompositeIntervalCaptureTest { + private val endpoint = "http://192.168.1.1:80/" + + @BeforeTest + fun setup() { + MockApiClient.status = HttpStatusCode.OK + } + + @AfterTest + fun teardown() { + MockApiClient.status = HttpStatusCode.OK + } + + /** + * call startCapture. + */ + @Test + fun startCaptureTest() = runTest { + // setup + val responseArray = arrayOf( + Resource("src/commonTest/resources/setOptions/set_options_done.json").readText(), + Resource("src/commonTest/resources/setOptions/set_options_done.json").readText(), + Resource("src/commonTest/resources/CompositeIntervalCapture/start_capture_progress.json").readText(), + Resource("src/commonTest/resources/CompositeIntervalCapture/start_capture_progress.json").readText(), + Resource("src/commonTest/resources/CompositeIntervalCapture/start_capture_progress.json").readText(), + Resource("src/commonTest/resources/CompositeIntervalCapture/start_capture_done.json").readText(), + ) + var counter = 0 + MockApiClient.onRequest = { request -> + val index = counter++ + + // check request + when (index) { + 0 -> { + CheckRequest.checkSetOptions(request = request, captureMode = CaptureMode.IMAGE) + } + + 1 -> { + CheckRequest.checkSetOptions(request = request, compositeShootingTime = 600) + } + + 2 -> { + CheckRequest.checkCommandName(request, "camera.startCapture") + } + } + + ByteReadChannel(responseArray[index]) + } + val deferred = CompletableDeferred() + + // execute + val thetaRepository = ThetaRepository(endpoint) + thetaRepository.cameraModel = ThetaRepository.ThetaModel.THETA_X + val capture = thetaRepository.getCompositeIntervalCaptureBuilder(600).build() + + var files: List? = null + capture.startCapture(object : CompositeIntervalCapture.StartCaptureCallback { + override fun onCaptureCompleted(fileUrls: List?) { + files = fileUrls + deferred.complete(Unit) + } + + override fun onProgress(completion: Float) { + assertTrue(completion >= 0f, "onProgress") + } + + override fun onCaptureFailed(exception: ThetaRepository.ThetaRepositoryException) { + assertTrue(false, "error start interval composite shooting") + deferred.complete(Unit) + } + + override fun onStopFailed(exception: ThetaRepository.ThetaRepositoryException) { + assertTrue(false, "onStopFailed") + } + }) + + runBlocking { + withTimeout(30_000) { + deferred.await() + } + } + + // check result + assertTrue( + files?.firstOrNull()?.startsWith("http://") ?: false, + "start capture interval composite shooting" + ) + } + + /** + * call cancelCapture test + */ + @Test + fun cancelCaptureTest() = runTest { + // setup + var isStop = false + MockApiClient.onRequest = { request -> + val path = if (request.body.toString().contains("camera.stopCapture")) { + isStop = true + "src/commonTest/resources/CompositeIntervalCapture/stop_capture_done.json" + } else if (request.body.toString().contains("camera.setOptions")) { + "src/commonTest/resources/setOptions/set_options_done.json" + } else { + if (isStop) + "src/commonTest/resources/CompositeIntervalCapture/start_capture_done_empty.json" + else + "src/commonTest/resources/CompositeIntervalCapture/start_capture_progress.json" + } + + ByteReadChannel(Resource(path).readText()) + } + + val deferred = CompletableDeferred() + + // execute + val thetaRepository = ThetaRepository(endpoint) + thetaRepository.cameraModel = ThetaRepository.ThetaModel.THETA_X + val capture = thetaRepository.getCompositeIntervalCaptureBuilder(600).build() + + var files: List? = null + val capturing = + capture.startCapture(object : CompositeIntervalCapture.StartCaptureCallback { + override fun onCaptureCompleted(fileUrls: List?) { + files = fileUrls + deferred.complete(Unit) + } + + override fun onProgress(completion: Float) { + assertEquals(completion, 0f, "onProgress") + } + + override fun onCaptureFailed(exception: ThetaRepository.ThetaRepositoryException) { + assertTrue(false, "error start interval composite shooting") + deferred.complete(Unit) + } + + override fun onStopFailed(exception: ThetaRepository.ThetaRepositoryException) { + assertTrue(false, "onStopFailed") + } + }) + + runBlocking { + delay(1000) + } + + capturing.cancelCapture() + + runBlocking { + withTimeout(7000) { + deferred.await() + } + } + + // check result + assertTrue( + files?.isEmpty() == true || files == null, + "cancel interval composite shooting" + ) + } + + /** + * Cancel shooting. + */ + @Test + fun cancelShootingTest() = runTest { + // setup + val responseArray = arrayOf( + Resource("src/commonTest/resources/setOptions/set_options_done.json").readText(), + Resource("src/commonTest/resources/setOptions/set_options_done.json").readText(), + Resource("src/commonTest/resources/CompositeIntervalCapture/start_capture_progress.json").readText(), + Resource("src/commonTest/resources/CompositeIntervalCapture/start_capture_progress.json").readText(), + Resource("src/commonTest/resources/CompositeIntervalCapture/start_capture_cancel.json").readText(), + ) + var counter = 0 + MockApiClient.onRequest = { _ -> + val index = counter++ + ByteReadChannel(responseArray[index]) + } + val deferred = CompletableDeferred() + + // execute + val thetaRepository = ThetaRepository(endpoint) + thetaRepository.cameraModel = ThetaRepository.ThetaModel.THETA_X + val capture = thetaRepository.getCompositeIntervalCaptureBuilder(600).build() + + var files: List? = listOf() + capture.startCapture(object : CompositeIntervalCapture.StartCaptureCallback { + override fun onCaptureCompleted(fileUrls: List?) { + files = fileUrls + deferred.complete(Unit) + } + + override fun onProgress(completion: Float) { + assertTrue(completion >= 0f, "onProgress") + } + + override fun onCaptureFailed(exception: ThetaRepository.ThetaRepositoryException) { + assertTrue(false, "error start interval composite shooting") + deferred.complete(Unit) + } + + override fun onStopFailed(exception: ThetaRepository.ThetaRepositoryException) { + assertTrue(false, "onStopFailed") + } + }) + + runBlocking { + withTimeout(30_000) { + deferred.await() + } + } + + // check result + assertNull(files, "shooting is canceled") + } + + /** + * Setting CheckStatusCommandInterval. + */ + @Test + fun settingCheckStatusCommandIntervalTest() = runTest { + val timeMillis = 1500L + + MockApiClient.onRequest = { + ByteReadChannel(Resource("src/commonTest/resources/setOptions/set_options_done.json").readText()) + } + + // execute + val thetaRepository = ThetaRepository(endpoint) + thetaRepository.cameraModel = ThetaRepository.ThetaModel.THETA_X + val capture = thetaRepository.getCompositeIntervalCaptureBuilder(600) + .setCheckStatusCommandInterval(timeMillis) + .build() + + // check result + assertEquals( + capture.getCheckStatusCommandInterval(), + timeMillis, + "set CheckStatusCommandInterval $timeMillis" + ) + } + + /** + * Setting captureInterval. + */ + @Test + fun settingCaptureIntervalTest() = runTest { + val interval = 60 + + // setup + val responseArray = arrayOf( + Resource("src/commonTest/resources/setOptions/set_options_done.json").readText(), + Resource("src/commonTest/resources/setOptions/set_options_done.json").readText() + ) + var counter = 0 + + MockApiClient.onRequest = { request -> + val index = counter++ + + // check request + when (index) { + 0 -> { + CheckRequest.checkSetOptions(request = request, captureMode = CaptureMode.IMAGE) + } + + 1 -> { + CheckRequest.checkSetOptions( + request = request, + compositeShootingOutputInterval = interval + ) + } + } + + ByteReadChannel(responseArray[index]) + } + + // execute + val thetaRepository = ThetaRepository(endpoint) + val capture = thetaRepository.getCompositeIntervalCaptureBuilder(600) + .setCompositeShootingOutputInterval(interval) + .build() + + // check result + assertEquals(capture.getCompositeShootingOutputInterval(), interval, "set option _compositeShootingOutputInterval $interval") + } + + /** + * Error response to build call. + */ + @Test + fun buildErrorResponseTest() = runTest { + // setup + val responseArray = arrayOf( + Resource("src/commonTest/resources/setOptions/set_options_error.json").readText(), // set captureMode error + "Not json" // json error + ) + var counter = 0 + + MockApiClient.onRequest = { _ -> + val index = counter++ + ByteReadChannel(responseArray[index]) + } + + // execute + val thetaRepository = ThetaRepository(endpoint) + thetaRepository.cameraModel = ThetaRepository.ThetaModel.THETA_X + + var exceptionSetCaptureMode = false + try { + thetaRepository.getCompositeIntervalCaptureBuilder(600).build() + } catch (e: ThetaRepository.ThetaWebApiException) { + assertTrue((e.message?.indexOf("UnitTest", 0, true) ?: -1) >= 0, "") + exceptionSetCaptureMode = true + } + assertTrue(exceptionSetCaptureMode, "setOptions captureMode error response") + + // execute not json response + var exceptionNotJson = false + try { + thetaRepository.getCompositeIntervalCaptureBuilder(600).build() + } catch (e: ThetaRepository.ThetaWebApiException) { + assertTrue( + (e.message?.indexOf("json", 0, true) ?: -1) >= 0 + || (e.message?.indexOf("Illegal", 0, true) ?: -1) >= 0, + "setOptions option not json error response" + ) + exceptionNotJson = true + } + assertTrue(exceptionNotJson, "setOptions option error response") + } + + /** + * Error exception to build call. + */ + @Test + fun buildExceptionTest() = runTest { + // setup + val responseArray = arrayOf( + Resource("src/commonTest/resources/setOptions/set_options_error.json").readText(), // status error & error json + "timeout UnitTest" // timeout + ) + var counter = 0 + + MockApiClient.onRequest = { _ -> + val index = counter++ + when (index) { + 0 -> MockApiClient.status = HttpStatusCode.ServiceUnavailable + 1 -> throw ConnectTimeoutException("timeout") + } + ByteReadChannel(responseArray[index]) + } + + // execute + val thetaRepository = ThetaRepository(endpoint) + thetaRepository.cameraModel = ThetaRepository.ThetaModel.THETA_X + + // execute status error and json response + var exceptionStatusJson = false + try { + thetaRepository.getCompositeIntervalCaptureBuilder(600).build() + } catch (e: ThetaRepository.ThetaWebApiException) { + assertTrue( + (e.message?.indexOf("UnitTest", 0, true) ?: -1) >= 0, + "status error and json response" + ) + exceptionStatusJson = true + } + assertTrue(exceptionStatusJson, "status error and json response") + + // execute timeout exception + var exceptionOther = false + try { + thetaRepository.getCompositeIntervalCaptureBuilder(600).build() + } catch (e: ThetaRepository.NotConnectedException) { + assertTrue((e.message?.indexOf("time", 0, true) ?: -1) >= 0, "timeout exception") + exceptionOther = true + } + assertTrue(exceptionOther, "other exception") + } + + /** + * Error response to startCapture call + */ + @Test + fun startCaptureErrorResponseTest() = runTest { + // setup + val responseArray = arrayOf( + Resource("src/commonTest/resources/setOptions/set_options_done.json").readText(), + Resource("src/commonTest/resources/setOptions/set_options_done.json").readText(), + Resource("src/commonTest/resources/CompositeIntervalCapture/start_capture_error.json").readText(), // startCapture error + "Not json" // json error + ) + var counter = 0 + MockApiClient.onRequest = { _ -> + val index = counter++ + ByteReadChannel(responseArray[index]) + } + + val thetaRepository = ThetaRepository(endpoint) + thetaRepository.cameraModel = ThetaRepository.ThetaModel.THETA_X + val capture = thetaRepository.getCompositeIntervalCaptureBuilder(600).build() + + // execute error response + var deferred = CompletableDeferred() + capture.startCapture(object : CompositeIntervalCapture.StartCaptureCallback { + override fun onCaptureCompleted(fileUrls: List?) { + assertTrue(false, "capture interval composite shooting") + deferred.complete(Unit) + } + + override fun onProgress(completion: Float) { + } + + override fun onCaptureFailed(exception: ThetaRepository.ThetaRepositoryException) { + assertTrue( + exception.message!!.indexOf("UnitTest", 0, true) >= 0, + "capture interval composite shooting error response" + ) + deferred.complete(Unit) + } + + override fun onStopFailed(exception: ThetaRepository.ThetaRepositoryException) { + assertTrue(false, "onStopFailed") + } + }) + + runBlocking { + withTimeout(500) { + deferred.await() + } + } + + // execute json error response + deferred = CompletableDeferred() + capture.startCapture(object : CompositeIntervalCapture.StartCaptureCallback { + override fun onCaptureCompleted(fileUrls: List?) { + assertTrue(false, "capture interval composite shooting") + deferred.complete(Unit) + } + + override fun onProgress(completion: Float) { + } + + override fun onCaptureFailed(exception: ThetaRepository.ThetaRepositoryException) { + assertTrue( + exception.message!!.length >= 0, + "capture interval composite shooting json error response" + ) + deferred.complete(Unit) + } + + override fun onStopFailed(exception: ThetaRepository.ThetaRepositoryException) { + assertTrue(false, "onStopFailed") + } + }) + + runBlocking { + withTimeout(2000) { + deferred.await() + } + } + } + + /** + * Error exception to startCapture call + */ + @Test + fun startCaptureExceptionTest() = runTest { + // setup + val responseArray = arrayOf( + Resource("src/commonTest/resources/setOptions/set_options_done.json").readText(), + Resource("src/commonTest/resources/setOptions/set_options_done.json").readText(), + Resource("src/commonTest/resources/CompositeIntervalCapture/start_capture_error.json").readText(), // startCapture error + "Status error UnitTest", // status error not json + "timeout UnitTest" // timeout + ) + var counter = 0 + MockApiClient.onRequest = { _ -> + val index = counter++ + when (index) { + 0 -> MockApiClient.status = HttpStatusCode.OK + 1 -> MockApiClient.status = HttpStatusCode.OK + 2 -> MockApiClient.status = HttpStatusCode.ServiceUnavailable + 3 -> MockApiClient.status = HttpStatusCode.ServiceUnavailable + 4 -> throw ConnectTimeoutException("timeout") + } + ByteReadChannel(responseArray[index]) + } + + val thetaRepository = ThetaRepository(endpoint) + val capture = thetaRepository.getCompositeIntervalCaptureBuilder(600) + .build() + + // execute status error and json response + var deferred = CompletableDeferred() + capture.startCapture(object : CompositeIntervalCapture.StartCaptureCallback { + override fun onCaptureCompleted(fileUrls: List?) { + assertTrue(false, "capture interval composite shooting") + deferred.complete(Unit) + } + + override fun onProgress(completion: Float) { + } + + override fun onCaptureFailed(exception: ThetaRepository.ThetaRepositoryException) { + assertTrue( + (exception.message?.indexOf("UnitTest", 0, true) ?: -1) >= 0, + "status error and json response" + ) + deferred.complete(Unit) + } + + override fun onStopFailed(exception: ThetaRepository.ThetaRepositoryException) { + assertTrue(false, "onStopFailed") + } + }) + + runBlocking { + withTimeout(1000) { + deferred.await() + } + } + + // execute status error and not json response + deferred = CompletableDeferred() + capture.startCapture(object : CompositeIntervalCapture.StartCaptureCallback { + override fun onCaptureCompleted(fileUrls: List?) { + assertTrue(false, "capture interval composite shooting") + deferred.complete(Unit) + } + + override fun onProgress(completion: Float) { + } + + override fun onCaptureFailed(exception: ThetaRepository.ThetaRepositoryException) { + assertTrue((exception.message?.indexOf("503", 0, true) ?: -1) >= 0, "status error") + deferred.complete(Unit) + } + + override fun onStopFailed(exception: ThetaRepository.ThetaRepositoryException) { + assertTrue(false, "onStopFailed") + } + }) + + runBlocking { + withTimeout(1000) { + deferred.await() + } + } + + // execute timeout exception + deferred = CompletableDeferred() + capture.startCapture(object : CompositeIntervalCapture.StartCaptureCallback { + override fun onCaptureCompleted(fileUrls: List?) { + assertTrue(false, "capture interval composite shooting") + deferred.complete(Unit) + } + + override fun onProgress(completion: Float) { + } + + override fun onCaptureFailed(exception: ThetaRepository.ThetaRepositoryException) { + assertTrue( + (exception.message?.indexOf("time", 0, true) ?: -1) >= 0, + "timeout exception" + ) + deferred.complete(Unit) + } + + override fun onStopFailed(exception: ThetaRepository.ThetaRepositoryException) { + assertTrue(false, "onStopFailed") + } + }) + + runBlocking { + withTimeout(1000) { + deferred.await() + } + } + } + + /** + * Error response to stopCapture call + */ + @Test + fun stopCaptureErrorResponseTest() = runTest { + // setup + val responseArray = arrayOf( + Resource("src/commonTest/resources/setOptions/set_options_done.json").readText(), + Resource("src/commonTest/resources/setOptions/set_options_done.json").readText(), + Resource("src/commonTest/resources/CompositeIntervalCapture/stop_capture_error.json").readText(), // stopCapture error + "Not json" // json error + ) + var counter = 0 + MockApiClient.onRequest = { _ -> + val index = counter++ + ByteReadChannel(responseArray[index]) + } + var deferred = CompletableDeferred() + + // execute + val thetaRepository = ThetaRepository(endpoint) + thetaRepository.cameraModel = ThetaRepository.ThetaModel.THETA_X + val capture = thetaRepository.getCompositeIntervalCaptureBuilder(600).build() + + var capturing = + capture.startCapture(object : CompositeIntervalCapture.StartCaptureCallback { + override fun onCaptureCompleted(fileUrls: List?) { + assertTrue(false, "capture interval composite shooting") + deferred.complete(Unit) + } + + override fun onProgress(completion: Float) { + } + + override fun onCaptureFailed(exception: ThetaRepository.ThetaRepositoryException) { + assertTrue( + (exception.message?.indexOf("UnitTest", 0, true) ?: -1) >= 0, + "stop capture error response" + ) + deferred.complete(Unit) + } + + override fun onStopFailed(exception: ThetaRepository.ThetaRepositoryException) { + assertTrue(false, "onStopFailed") + } + }) + + runBlocking { + delay(100) + } + + capturing.cancelCapture() + + runBlocking { + withTimeout(1000) { + deferred.await() + } + } + + deferred = CompletableDeferred() + capturing = + capture.startCapture(object : CompositeIntervalCapture.StartCaptureCallback { + override fun onCaptureCompleted(fileUrls: List?) { + assertTrue(false, "capture interval composite shooting") + deferred.complete(Unit) + } + + override fun onProgress(completion: Float) { + } + + override fun onCaptureFailed(exception: ThetaRepository.ThetaRepositoryException) { + assertTrue( + (exception.message?.length ?: -1) >= 0, + "stop capture json error response" + ) + deferred.complete(Unit) + } + + override fun onStopFailed(exception: ThetaRepository.ThetaRepositoryException) { + assertTrue(false, "onStopFailed") + } + }) + + runBlocking { + delay(100) + } + + capturing.cancelCapture() + + runBlocking { + withTimeout(1000) { + deferred.await() + } + } + } + + /** + * Error exception to stopCapture call + */ + @Test + fun stopCaptureExceptionTest() = runTest { + // setup + val responseArray = arrayOf( + Resource("src/commonTest/resources/CompositeIntervalCapture/stop_capture_error.json").readText(), // status error & error json + "Status error UnitTest", // status error not json + "timeout UnitTest" // timeout + ) + var counter = 0 + MockApiClient.onRequest = { _ -> + val index = counter++ + when (index) { + 1 -> MockApiClient.status = HttpStatusCode.ServiceUnavailable + 2 -> MockApiClient.status = HttpStatusCode.ServiceUnavailable + 3 -> throw ConnectTimeoutException("timeout") + } + ByteReadChannel(responseArray[index]) + } + + var deferred = CompletableDeferred() + + var capturing = CompositeIntervalCapturing( + endpoint, + object : CompositeIntervalCapture.StartCaptureCallback { + override fun onCaptureCompleted(fileUrls: List?) { + assertTrue(false, "capture interval composite shooting") + deferred.complete(Unit) + } + + override fun onProgress(completion: Float) { + } + + override fun onCaptureFailed(exception: ThetaRepository.ThetaRepositoryException) { + assertTrue(false, "onCaptureFailed") + } + + override fun onStopFailed(exception: ThetaRepository.ThetaRepositoryException) { + assertTrue( + (exception.message?.indexOf("UnitTest", 0, true) ?: -1) >= 0, + "status error and json response" + ) + deferred.complete(Unit) + } + }) + + capturing.cancelCapture() + + runBlocking { + withTimeout(1000) { + deferred.await() + } + } + + deferred = CompletableDeferred() + capturing = CompositeIntervalCapturing( + endpoint, + object : CompositeIntervalCapture.StartCaptureCallback { + override fun onCaptureCompleted(fileUrls: List?) { + assertTrue(false, "capture interval composite shooting") + deferred.complete(Unit) + } + + override fun onProgress(completion: Float) { + } + + override fun onCaptureFailed(exception: ThetaRepository.ThetaRepositoryException) { + assertTrue(false, "onCaptureFailed") + } + + override fun onStopFailed(exception: ThetaRepository.ThetaRepositoryException) { + assertTrue( + (exception.message?.indexOf("503", 0, true) ?: -1) >= 0, + "status error" + ) + deferred.complete(Unit) + } + }) + + capturing.cancelCapture() + + runBlocking { + withTimeout(1000) { + deferred.await() + } + } + + deferred = CompletableDeferred() + capturing = CompositeIntervalCapturing( + endpoint, + object : CompositeIntervalCapture.StartCaptureCallback { + override fun onCaptureCompleted(fileUrls: List?) { + assertTrue(false, "capture interval composite shooting") + deferred.complete(Unit) + } + + override fun onProgress(completion: Float) { + } + + override fun onCaptureFailed(exception: ThetaRepository.ThetaRepositoryException) { + assertTrue(false, "onCaptureFailed") + } + + override fun onStopFailed(exception: ThetaRepository.ThetaRepositoryException) { + assertTrue( + (exception.message?.indexOf("time", 0, true) ?: -1) >= 0, + "timeout exception" + ) + deferred.complete(Unit) + } + }) + + capturing.cancelCapture() + + runBlocking { + withTimeout(5000) { + deferred.await() + } + } + } +} diff --git a/kotlin-multiplatform/src/commonTest/kotlin/com/ricoh360/thetaclient/capture/TimeShiftCaptureTest.kt b/kotlin-multiplatform/src/commonTest/kotlin/com/ricoh360/thetaclient/capture/TimeShiftCaptureTest.kt index 581384db23..498fb2dad7 100644 --- a/kotlin-multiplatform/src/commonTest/kotlin/com/ricoh360/thetaclient/capture/TimeShiftCaptureTest.kt +++ b/kotlin-multiplatform/src/commonTest/kotlin/com/ricoh360/thetaclient/capture/TimeShiftCaptureTest.kt @@ -79,7 +79,7 @@ class TimeShiftCaptureTest { var file: String? = null timeShiftCapture.startCapture(object : TimeShiftCapture.StartCaptureCallback { - override fun onSuccess(fileUrl: String?) { + override fun onCaptureCompleted(fileUrl: String?) { file = fileUrl deferred.complete(Unit) } @@ -88,7 +88,11 @@ class TimeShiftCaptureTest { assertTrue(completion >= 0f, "onProgress") } - override fun onError(exception: ThetaRepository.ThetaRepositoryException) { + override fun onCaptureFailed(exception: ThetaRepository.ThetaRepositoryException) { + assertTrue(false, "error start time-shift") + deferred.complete(Unit) + } + override fun onStopFailed(exception: ThetaRepository.ThetaRepositoryException) { assertTrue(false, "error start time-shift") deferred.complete(Unit) } @@ -153,7 +157,7 @@ class TimeShiftCaptureTest { var file: String? = null timeShiftCapture.startCapture(object : TimeShiftCapture.StartCaptureCallback { - override fun onSuccess(fileUrl: String?) { + override fun onCaptureCompleted(fileUrl: String?) { file = fileUrl deferred.complete(Unit) } @@ -162,7 +166,11 @@ class TimeShiftCaptureTest { assertTrue(completion >= 0f, "onProgress") } - override fun onError(exception: ThetaRepository.ThetaRepositoryException) { + override fun onCaptureFailed(exception: ThetaRepository.ThetaRepositoryException) { + assertTrue(false, "error start time-shift") + deferred.complete(Unit) + } + override fun onStopFailed(exception: ThetaRepository.ThetaRepositoryException) { assertTrue(false, "error start time-shift") deferred.complete(Unit) } @@ -227,7 +235,7 @@ class TimeShiftCaptureTest { var file: String? = null timeShiftCapture.startCapture(object : TimeShiftCapture.StartCaptureCallback { - override fun onSuccess(fileUrl: String?) { + override fun onCaptureCompleted(fileUrl: String?) { file = fileUrl deferred.complete(Unit) } @@ -236,7 +244,12 @@ class TimeShiftCaptureTest { assertTrue(completion >= 0f, "onProgress") } - override fun onError(exception: ThetaRepository.ThetaRepositoryException) { + override fun onCaptureFailed(exception: ThetaRepository.ThetaRepositoryException) { + assertTrue(false, "error start time-shift") + deferred.complete(Unit) + } + + override fun onStopFailed(exception: ThetaRepository.ThetaRepositoryException) { assertTrue(false, "error start time-shift") deferred.complete(Unit) } @@ -284,7 +297,7 @@ class TimeShiftCaptureTest { var file: String? = null val capturing = timeShiftCapture.startCapture(object : TimeShiftCapture.StartCaptureCallback { - override fun onSuccess(fileUrl: String?) { + override fun onCaptureCompleted(fileUrl: String?) { file = fileUrl deferred.complete(Unit) } @@ -293,7 +306,12 @@ class TimeShiftCaptureTest { assertEquals(completion, 0f, "onProgress") } - override fun onError(exception: ThetaRepository.ThetaRepositoryException) { + override fun onCaptureFailed(exception: ThetaRepository.ThetaRepositoryException) { + assertTrue(false, "error start time-shift") + deferred.complete(Unit) + } + + override fun onStopFailed(exception: ThetaRepository.ThetaRepositoryException) { assertTrue(false, "error start time-shift") deferred.complete(Unit) } @@ -340,7 +358,7 @@ class TimeShiftCaptureTest { var file: String? = "" timeShiftCapture.startCapture(object : TimeShiftCapture.StartCaptureCallback { - override fun onSuccess(fileUrl: String?) { + override fun onCaptureCompleted(fileUrl: String?) { file = fileUrl deferred.complete(Unit) } @@ -349,7 +367,12 @@ class TimeShiftCaptureTest { assertTrue(completion >= 0f, "onProgress") } - override fun onError(exception: ThetaRepository.ThetaRepositoryException) { + override fun onCaptureFailed(exception: ThetaRepository.ThetaRepositoryException) { + assertTrue(false, "error start time-shift") + deferred.complete(Unit) + } + + override fun onStopFailed(exception: ThetaRepository.ThetaRepositoryException) { assertTrue(false, "error start time-shift") deferred.complete(Unit) } @@ -391,7 +414,7 @@ class TimeShiftCaptureTest { var file: String? = "" timeShiftCapture.startCapture(object : TimeShiftCapture.StartCaptureCallback { - override fun onSuccess(fileUrl: String?) { + override fun onCaptureCompleted(fileUrl: String?) { file = fileUrl deferred.complete(Unit) } @@ -400,7 +423,12 @@ class TimeShiftCaptureTest { assertTrue(completion >= 0f, "onProgress") } - override fun onError(exception: ThetaRepository.ThetaRepositoryException) { + override fun onCaptureFailed(exception: ThetaRepository.ThetaRepositoryException) { + assertTrue(false, "error start time-shift") + deferred.complete(Unit) + } + + override fun onStopFailed(exception: ThetaRepository.ThetaRepositoryException) { assertTrue(false, "error start time-shift") deferred.complete(Unit) } @@ -646,7 +674,7 @@ class TimeShiftCaptureTest { // execute error response var deferred = CompletableDeferred() capture.startCapture(object : TimeShiftCapture.StartCaptureCallback { - override fun onSuccess(fileUrl: String?) { + override fun onCaptureCompleted(fileUrl: String?) { assertTrue(false, "capture time-shift") deferred.complete(Unit) } @@ -654,10 +682,15 @@ class TimeShiftCaptureTest { override fun onProgress(completion: Float) { } - override fun onError(exception: ThetaRepository.ThetaRepositoryException) { + override fun onCaptureFailed(exception: ThetaRepository.ThetaRepositoryException) { assertTrue((exception.message?.indexOf("UnitTest", 0, true) ?: -1) >= 0, "capture time-shift error response") deferred.complete(Unit) } + + override fun onStopFailed(exception: ThetaRepository.ThetaRepositoryException) { + assertTrue(false, "capture time-shift") + deferred.complete(Unit) + } }) runBlocking { @@ -669,7 +702,7 @@ class TimeShiftCaptureTest { // execute json error response deferred = CompletableDeferred() capture.startCapture(object : TimeShiftCapture.StartCaptureCallback { - override fun onSuccess(fileUrl: String?) { + override fun onCaptureCompleted(fileUrl: String?) { assertTrue(false, "capture time-shift") deferred.complete(Unit) } @@ -677,10 +710,15 @@ class TimeShiftCaptureTest { override fun onProgress(completion: Float) { } - override fun onError(exception: ThetaRepository.ThetaRepositoryException) { + override fun onCaptureFailed(exception: ThetaRepository.ThetaRepositoryException) { assertTrue((exception.message?.length ?: -1) >= 0, "capture time-shift json error response") deferred.complete(Unit) } + + override fun onStopFailed(exception: ThetaRepository.ThetaRepositoryException) { + assertTrue(false, "capture time-shift") + deferred.complete(Unit) + } }) runBlocking { @@ -721,7 +759,7 @@ class TimeShiftCaptureTest { // execute status error and json response var deferred = CompletableDeferred() capture.startCapture(object : TimeShiftCapture.StartCaptureCallback { - override fun onSuccess(fileUrl: String?) { + override fun onCaptureCompleted(fileUrl: String?) { assertTrue(false, "capture time-shift") deferred.complete(Unit) } @@ -729,10 +767,15 @@ class TimeShiftCaptureTest { override fun onProgress(completion: Float) { } - override fun onError(exception: ThetaRepository.ThetaRepositoryException) { + override fun onCaptureFailed(exception: ThetaRepository.ThetaRepositoryException) { assertTrue((exception.message?.indexOf("UnitTest", 0, true) ?: -1) >= 0, "status error and json response") deferred.complete(Unit) } + + override fun onStopFailed(exception: ThetaRepository.ThetaRepositoryException) { + assertTrue(false, "capture time-shift") + deferred.complete(Unit) + } }) runBlocking { @@ -742,9 +785,9 @@ class TimeShiftCaptureTest { } // execute status error and not json response - deferred = CompletableDeferred() + deferred = CompletableDeferred() capture.startCapture(object : TimeShiftCapture.StartCaptureCallback { - override fun onSuccess(fileUrl: String?) { + override fun onCaptureCompleted(fileUrl: String?) { assertTrue(false, "capture time-shift") deferred.complete(Unit) } @@ -752,10 +795,15 @@ class TimeShiftCaptureTest { override fun onProgress(completion: Float) { } - override fun onError(exception: ThetaRepository.ThetaRepositoryException) { + override fun onCaptureFailed(exception: ThetaRepository.ThetaRepositoryException) { assertTrue((exception.message?.indexOf("503", 0, true) ?: -1) >= 0, "status error") deferred.complete(Unit) } + + override fun onStopFailed(exception: ThetaRepository.ThetaRepositoryException) { + assertTrue(false, "capture time-shift") + deferred.complete(Unit) + } }) runBlocking { @@ -765,9 +813,9 @@ class TimeShiftCaptureTest { } // execute timeout exception - deferred = CompletableDeferred() + deferred = CompletableDeferred() capture.startCapture(object : TimeShiftCapture.StartCaptureCallback { - override fun onSuccess(fileUrl: String?) { + override fun onCaptureCompleted(fileUrl: String?) { assertTrue(false, "capture time-shift") deferred.complete(Unit) } @@ -775,10 +823,15 @@ class TimeShiftCaptureTest { override fun onProgress(completion: Float) { } - override fun onError(exception: ThetaRepository.ThetaRepositoryException) { + override fun onCaptureFailed(exception: ThetaRepository.ThetaRepositoryException) { assertTrue((exception.message?.indexOf("time", 0, true) ?: -1) >= 0, "timeout exception") deferred.complete(Unit) } + + override fun onStopFailed(exception: ThetaRepository.ThetaRepositoryException) { + assertTrue(false, "capture time-shift") + deferred.complete(Unit) + } }) runBlocking { @@ -796,7 +849,7 @@ class TimeShiftCaptureTest { // setup val responseArray = arrayOf( Resource("src/commonTest/resources/setOptions/set_options_done.json").readText(), - Resource("src/commonTest/resources/setOptions/set_options_done.json").readText(), + Resource("src/commonTest/resources/TimeShiftCapture/start_capture_progress.json").readText(), Resource("src/commonTest/resources/TimeShiftCapture/stop_capture_error.json").readText(), // stopCapture error "Not json" // json error ) @@ -805,15 +858,15 @@ class TimeShiftCaptureTest { val index = counter++ ByteReadChannel(responseArray[index]) } - var deferred = CompletableDeferred() + val deferred = CompletableDeferred() // execute val thetaRepository = ThetaRepository(endpoint) thetaRepository.cameraModel = ThetaRepository.ThetaModel.THETA_X val timeShiftCapture = thetaRepository.getTimeShiftCaptureBuilder().build() - var capturing = timeShiftCapture.startCapture(object : TimeShiftCapture.StartCaptureCallback { - override fun onSuccess(fileUrl: String?) { + val capturing = timeShiftCapture.startCapture(object : TimeShiftCapture.StartCaptureCallback { + override fun onCaptureCompleted(fileUrl: String?) { assertTrue(false, "capture time-shift") deferred.complete(Unit) } @@ -821,37 +874,13 @@ class TimeShiftCaptureTest { override fun onProgress(completion: Float) { } - override fun onError(exception: ThetaRepository.ThetaRepositoryException) { - assertTrue((exception.message?.indexOf("UnitTest", 0, true) ?: -1) >= 0, "stop capture error response") + override fun onCaptureFailed(exception: ThetaRepository.ThetaRepositoryException) { + assertTrue((exception.message?.indexOf("Unknown", 0, true) ?: -1) >= 0, "stop capture error response") deferred.complete(Unit) } - }) - - runBlocking { - delay(100) - } - - capturing.cancelCapture() - - runBlocking { - withTimeout(1000) { - deferred.await() - } - } - deferred = CompletableDeferred() - capturing = timeShiftCapture.startCapture(object : TimeShiftCapture.StartCaptureCallback { - override fun onSuccess(fileUrl: String?) { - assertTrue(false, "capture time-shift") - deferred.complete(Unit) - } - - override fun onProgress(completion: Float) { - } - - override fun onError(exception: ThetaRepository.ThetaRepositoryException) { - assertTrue((exception.message?.length ?: -1) >= 0, "stop capture json error response") - deferred.complete(Unit) + override fun onStopFailed(exception: ThetaRepository.ThetaRepositoryException) { + assertTrue((exception.message?.indexOf("UnitTest", 0, true) ?: -1) >= 0, "stop capture error response") } }) @@ -862,7 +891,7 @@ class TimeShiftCaptureTest { capturing.cancelCapture() runBlocking { - withTimeout(1000) { + withTimeout(2000) { deferred.await() } } @@ -893,7 +922,7 @@ class TimeShiftCaptureTest { var deferred = CompletableDeferred() var capturing = TimeShiftCapturing(endpoint, object : TimeShiftCapture.StartCaptureCallback { - override fun onSuccess(fileUrl: String?) { + override fun onCaptureCompleted(fileUrl: String?) { assertTrue(false, "capture time-shift") deferred.complete(Unit) } @@ -901,7 +930,12 @@ class TimeShiftCaptureTest { override fun onProgress(completion: Float) { } - override fun onError(exception: ThetaRepository.ThetaRepositoryException) { + override fun onCaptureFailed(exception: ThetaRepository.ThetaRepositoryException) { + assertTrue(false, "capture time-shift") + deferred.complete(Unit) + } + + override fun onStopFailed(exception: ThetaRepository.ThetaRepositoryException) { assertTrue((exception.message?.indexOf("UnitTest", 0, true) ?: -1) >= 0, "status error and json response") deferred.complete(Unit) } @@ -917,7 +951,7 @@ class TimeShiftCaptureTest { deferred = CompletableDeferred() capturing = TimeShiftCapturing(endpoint, object : TimeShiftCapture.StartCaptureCallback { - override fun onSuccess(fileUrl: String?) { + override fun onCaptureCompleted(fileUrl: String?) { assertTrue(false, "capture time-shift") deferred.complete(Unit) } @@ -925,7 +959,12 @@ class TimeShiftCaptureTest { override fun onProgress(completion: Float) { } - override fun onError(exception: ThetaRepository.ThetaRepositoryException) { + override fun onCaptureFailed(exception: ThetaRepository.ThetaRepositoryException) { + assertTrue(false, "capture time-shift") + deferred.complete(Unit) + } + + override fun onStopFailed(exception: ThetaRepository.ThetaRepositoryException) { assertTrue((exception.message?.indexOf("503", 0, true) ?: -1) >= 0, "status error") deferred.complete(Unit) } @@ -941,7 +980,7 @@ class TimeShiftCaptureTest { deferred = CompletableDeferred() capturing = TimeShiftCapturing(endpoint, object : TimeShiftCapture.StartCaptureCallback { - override fun onSuccess(fileUrl: String?) { + override fun onCaptureCompleted(fileUrl: String?) { assertTrue(false, "capture time-shift") deferred.complete(Unit) } @@ -949,7 +988,12 @@ class TimeShiftCaptureTest { override fun onProgress(completion: Float) { } - override fun onError(exception: ThetaRepository.ThetaRepositoryException) { + override fun onCaptureFailed(exception: ThetaRepository.ThetaRepositoryException) { + assertTrue(false, "capture time-shift") + deferred.complete(Unit) + } + + override fun onStopFailed(exception: ThetaRepository.ThetaRepositoryException) { assertTrue((exception.message?.indexOf("time", 0, true) ?: -1) >= 0, "timeout exception") deferred.complete(Unit) } diff --git a/kotlin-multiplatform/src/commonTest/kotlin/com/ricoh360/thetaclient/repository/options/AutoBracketTest.kt b/kotlin-multiplatform/src/commonTest/kotlin/com/ricoh360/thetaclient/repository/options/AutoBracketTest.kt new file mode 100644 index 0000000000..102facb68d --- /dev/null +++ b/kotlin-multiplatform/src/commonTest/kotlin/com/ricoh360/thetaclient/repository/options/AutoBracketTest.kt @@ -0,0 +1,197 @@ +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.AutoBracket +import com.ricoh360.thetaclient.transferred.BracketParameter +import com.ricoh360.thetaclient.transferred.Options +import com.ricoh360.thetaclient.transferred.WhiteBalance +import io.ktor.http.* +import io.ktor.utils.io.* +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +@OptIn(ExperimentalCoroutinesApi::class) +class AutoBracketTest { + 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 autoBracket. + */ + @Test + fun getOptionAutoBracketTest() = runTest { + val optionNames = listOf( + ThetaRepository.OptionNameEnum.AutoBracket + ) + val stringOptionNames = listOf( + "_autoBracket" + ) + + MockApiClient.onRequest = { request -> + // check request + CheckRequest.checkGetOptions(request, stringOptionNames) + ByteReadChannel(Resource("src/commonTest/resources/options/option_auto_bracket.json").readText()) + } + + val thetaRepository = ThetaRepository(endpoint) + val options = thetaRepository.getOptions(optionNames) + assertNotNull(options.autoBracket, "autoBracket null") + assertEquals(options.autoBracket!!.list.size, 2, "autoBracket size") + assertEquals(options.autoBracket!!.list[0].colorTemperature, 5000, "autoBracket") + assertEquals( + options.autoBracket!!.list[0].exposureCompensation, + ThetaRepository.ExposureCompensationEnum.ZERO, + "autoBracket" + ) + assertEquals( + options.autoBracket!!.list[0].exposureProgram, + ThetaRepository.ExposureProgramEnum.MANUAL, + "autoBracket" + ) + assertEquals( + options.autoBracket!!.list[0].iso, + ThetaRepository.IsoEnum.ISO_400, + "autoBracket" + ) + assertEquals( + options.autoBracket!!.list[0].shutterSpeed, + ThetaRepository.ShutterSpeedEnum.SHUTTER_SPEED_ONE_OVER_250, + "autoBracket" + ) + assertEquals( + options.autoBracket!!.list[0].whiteBalance, + ThetaRepository.WhiteBalanceEnum.AUTO, + "autoBracket" + ) + } + + /** + * Set option autoBracket. + */ + @Test + fun setOptionAutoBracketTest() = runTest { + //val value = Pair(ThetaRepository.AutoBracketEnum.=MEMBER=, AutoBracket.=MEMBER=) + val list = ThetaRepository.BracketSettingList() + list.add( + ThetaRepository.BracketSetting( + exposureProgram = ThetaRepository.ExposureProgramEnum.NORMAL_PROGRAM, + whiteBalance = ThetaRepository.WhiteBalanceEnum.DAYLIGHT, + ) + ).add( + ThetaRepository.BracketSetting( + aperture = ThetaRepository.ApertureEnum.APERTURE_2_0, + colorTemperature = 6800, + exposureProgram = ThetaRepository.ExposureProgramEnum.MANUAL, + iso = ThetaRepository.IsoEnum.ISO_800, + shutterSpeed = ThetaRepository.ShutterSpeedEnum.SHUTTER_SPEED_ONE_OVER_100, + whiteBalance = ThetaRepository.WhiteBalanceEnum.CLOUDY_DAYLIGHT, + ) + ) + + val transferred = AutoBracket( + 2, listOf( + BracketParameter(exposureProgram = 2, whiteBalance = WhiteBalance.DAYLIGHT), + BracketParameter( + aperture = 2.0F, + _colorTemperature = 6800, + exposureProgram = 1, + iso = 800, + shutterSpeed = 0.01, + whiteBalance = WhiteBalance.CLOUDY_DAYLIGHT + ) + ) + ) + + + MockApiClient.onRequest = { request -> + // check request + CheckRequest.checkSetOptions(request, autoBracket = transferred) + ByteReadChannel(Resource("src/commonTest/resources/setOptions/set_options_done.json").readText()) + } + + val thetaRepository = ThetaRepository(endpoint) + val options = ThetaRepository.Options( + autoBracket = list + ) + kotlin.runCatching { + thetaRepository.setOptions(options) + }.onSuccess { + assertTrue(true, "setOptions AutoBracket") + }.onFailure { + println(it.toString()) + assertTrue(false, "setOptions AutoBracket") + } + } + + /** + * Convert ThetaRepository.Options to Options. + */ + @Test + fun convertOptionAutoBracketTest() = runTest { + val values = listOf( + Pair( + ThetaRepository.BracketSettingList().add( + ThetaRepository.BracketSetting( + exposureProgram = ThetaRepository.ExposureProgramEnum.NORMAL_PROGRAM, + whiteBalance = ThetaRepository.WhiteBalanceEnum.DAYLIGHT, + ) + ).add( + ThetaRepository.BracketSetting( + aperture = ThetaRepository.ApertureEnum.APERTURE_2_0, + colorTemperature = 6800, + exposureProgram = ThetaRepository.ExposureProgramEnum.MANUAL, + iso = ThetaRepository.IsoEnum.ISO_800, + shutterSpeed = ThetaRepository.ShutterSpeedEnum.SHUTTER_SPEED_ONE_OVER_100, + whiteBalance = ThetaRepository.WhiteBalanceEnum.CLOUDY_DAYLIGHT, + ) + ), + AutoBracket( + 2, listOf( + BracketParameter(exposureProgram = 2, whiteBalance = WhiteBalance.DAYLIGHT), + BracketParameter( + aperture = 2.0F, + _colorTemperature = 6800, + exposureProgram = 1, + iso = 800, + shutterSpeed = 0.01, + whiteBalance = WhiteBalance.CLOUDY_DAYLIGHT, + ) + ) + ), + ), + ) + + values.forEach { + val orgOptions = Options( + _autoBracket = it.second + ) + val options = ThetaRepository.Options(orgOptions) + assertEquals(options.autoBracket, it.first, "autoBracket ${it.second}") + } + values.forEach { + val orgOptions = ThetaRepository.Options( + autoBracket = it.first + ) + val options = orgOptions.toOptions() + assertEquals(options._autoBracket, it.second, "autoBracket ${it.second}") + } + } +} 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 ae097b525b..b990e4feca 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 @@ -2,7 +2,9 @@ package com.ricoh360.thetaclient.repository.options import com.ricoh360.thetaclient.ThetaRepository import com.ricoh360.thetaclient.transferred.AiAutoThumbnail +import com.ricoh360.thetaclient.transferred.AutoBracket import com.ricoh360.thetaclient.transferred.BluetoothPower +import com.ricoh360.thetaclient.transferred.BracketParameter import com.ricoh360.thetaclient.transferred.BurstBracketStep import com.ricoh360.thetaclient.transferred.BurstCaptureNum import com.ricoh360.thetaclient.transferred.BurstCompensation @@ -61,6 +63,21 @@ class OptionsTest { fun optionsPrimaryConstructorTest() { val aiAutoThumbnail = ThetaRepository.AiAutoThumbnailEnum.ON val aperture = ThetaRepository.ApertureEnum.APERTURE_2_1 + val autoBracket = ThetaRepository.BracketSettingList().add( + ThetaRepository.BracketSetting( + exposureProgram = ThetaRepository.ExposureProgramEnum.NORMAL_PROGRAM, + whiteBalance = ThetaRepository.WhiteBalanceEnum.DAYLIGHT, + ) + ).add( + ThetaRepository.BracketSetting( + aperture = ThetaRepository.ApertureEnum.APERTURE_2_0, + colorTemperature = 6800, + exposureProgram = ThetaRepository.ExposureProgramEnum.MANUAL, + iso = ThetaRepository.IsoEnum.ISO_800, + shutterSpeed = ThetaRepository.ShutterSpeedEnum.SHUTTER_SPEED_ONE_OVER_100, + whiteBalance = ThetaRepository.WhiteBalanceEnum.CLOUDY_DAYLIGHT, + ) + ) val bitrate = ThetaRepository.BitrateEnum.FINE val bluetoothPower = ThetaRepository.BluetoothPowerEnum.ON val burstMode = ThetaRepository.BurstModeEnum.ON @@ -130,6 +147,7 @@ class OptionsTest { val options = ThetaRepository.Options( aiAutoThumbnail = aiAutoThumbnail, aperture = aperture, + autoBracket = autoBracket, bitrate = bitrate, bluetoothPower = bluetoothPower, burstMode = burstMode, @@ -192,6 +210,7 @@ class OptionsTest { assertEquals(options.getValue(ThetaRepository.OptionNameEnum.AiAutoThumbnail), aiAutoThumbnail, "aiAutoThumbnail") assertEquals(options.getValue(ThetaRepository.OptionNameEnum.Aperture), aperture, "aperture") + assertEquals(options.getValue(ThetaRepository.OptionNameEnum.AutoBracket), autoBracket, "autoBracket") assertEquals(options.getValue(ThetaRepository.OptionNameEnum.Bitrate), bitrate, "bitrate") assertEquals(options.getValue(ThetaRepository.OptionNameEnum.BluetoothPower), bluetoothPower, "bluetoothPower") assertEquals(options.getValue(ThetaRepository.OptionNameEnum.BurstMode), burstMode, "burstMode") @@ -256,6 +275,23 @@ class OptionsTest { val values = listOf( Pair(ThetaRepository.OptionNameEnum.AiAutoThumbnail, ThetaRepository.AiAutoThumbnailEnum.OFF), Pair(ThetaRepository.OptionNameEnum.Aperture, ThetaRepository.ApertureEnum.APERTURE_2_1), + Pair(ThetaRepository.OptionNameEnum.AutoBracket, + ThetaRepository.BracketSettingList().add( + ThetaRepository.BracketSetting( + exposureProgram = ThetaRepository.ExposureProgramEnum.NORMAL_PROGRAM, + whiteBalance = ThetaRepository.WhiteBalanceEnum.DAYLIGHT, + ) + ).add( + ThetaRepository.BracketSetting( + aperture = ThetaRepository.ApertureEnum.APERTURE_2_0, + colorTemperature = 6800, + exposureProgram = ThetaRepository.ExposureProgramEnum.MANUAL, + iso = ThetaRepository.IsoEnum.ISO_800, + shutterSpeed = ThetaRepository.ShutterSpeedEnum.SHUTTER_SPEED_ONE_OVER_100, + whiteBalance = ThetaRepository.WhiteBalanceEnum.CLOUDY_DAYLIGHT, + ) + ) + ), Pair(ThetaRepository.OptionNameEnum.Bitrate, ThetaRepository.BitrateEnum.FINE), Pair(ThetaRepository.OptionNameEnum.BluetoothPower, ThetaRepository.BluetoothPowerEnum.ON), Pair(ThetaRepository.OptionNameEnum.BurstMode, ThetaRepository.BurstModeEnum.ON), @@ -339,6 +375,36 @@ class OptionsTest { fun optionsSecondaryConstructorTest() { val aiAutoThumbnail = Pair(AiAutoThumbnail.OFF, ThetaRepository.AiAutoThumbnailEnum.OFF) val aperture = Pair(2.1f, ThetaRepository.ApertureEnum.APERTURE_2_1) + val autoBracket = Pair( + AutoBracket( + 2, listOf( + BracketParameter(exposureProgram = 2, whiteBalance = WhiteBalance.DAYLIGHT), + BracketParameter( + aperture = 2.0F, + _colorTemperature = 6800, + exposureProgram = 1, + iso = 800, + shutterSpeed = 0.01, + whiteBalance = WhiteBalance.CLOUDY_DAYLIGHT, + ) + ) + ), + ThetaRepository.BracketSettingList().add( + ThetaRepository.BracketSetting( + exposureProgram = ThetaRepository.ExposureProgramEnum.NORMAL_PROGRAM, + whiteBalance = ThetaRepository.WhiteBalanceEnum.DAYLIGHT, + ) + ).add( + ThetaRepository.BracketSetting( + aperture = ThetaRepository.ApertureEnum.APERTURE_2_0, + colorTemperature = 6800, + exposureProgram = ThetaRepository.ExposureProgramEnum.MANUAL, + iso = ThetaRepository.IsoEnum.ISO_800, + shutterSpeed = ThetaRepository.ShutterSpeedEnum.SHUTTER_SPEED_ONE_OVER_100, + whiteBalance = ThetaRepository.WhiteBalanceEnum.CLOUDY_DAYLIGHT, + ) + ) + ) val bitrate = Pair("Fine", ThetaRepository.BitrateEnum.FINE) val bluetoothPower = Pair(BluetoothPower.ON, ThetaRepository.BluetoothPowerEnum.ON) val burstMode = Pair(BurstMode.ON, ThetaRepository.BurstModeEnum.ON) @@ -417,6 +483,7 @@ class OptionsTest { val orgOptions = Options( _aiAutoThumbnail = aiAutoThumbnail.first, aperture = aperture.first, + _autoBracket = autoBracket.first, _bitrate = bitrate.first, _bluetoothPower = bluetoothPower.first, _burstMode = burstMode.first, @@ -474,6 +541,7 @@ class OptionsTest { assertEquals(options.aiAutoThumbnail, aiAutoThumbnail.second, "aiAutoThumbnail") assertEquals(options.aperture, aperture.second, "aperture") + assertEquals(options.autoBracket, autoBracket.second, "autoBracket") assertEquals(options.bitrate, bitrate.second, "bitrate") assertEquals(options.bluetoothPower, bluetoothPower.second, "bluetoothPower") assertEquals(options.burstMode, burstMode.second, "burstMode") @@ -532,6 +600,36 @@ class OptionsTest { fun optionsConvertTest() { val aiAutoThumbnail = Pair(AiAutoThumbnail.ON, ThetaRepository.AiAutoThumbnailEnum.ON) val aperture = Pair(2.1f, ThetaRepository.ApertureEnum.APERTURE_2_1) + val autoBracket = Pair( + AutoBracket( + 2, listOf( + BracketParameter(exposureProgram = 2, whiteBalance = WhiteBalance.DAYLIGHT), + BracketParameter( + aperture = 2.0F, + _colorTemperature = 6800, + exposureProgram = 1, + iso = 800, + shutterSpeed = 0.01, + whiteBalance = WhiteBalance.CLOUDY_DAYLIGHT, + ) + ) + ), + ThetaRepository.BracketSettingList().add( + ThetaRepository.BracketSetting( + exposureProgram = ThetaRepository.ExposureProgramEnum.NORMAL_PROGRAM, + whiteBalance = ThetaRepository.WhiteBalanceEnum.DAYLIGHT, + ) + ).add( + ThetaRepository.BracketSetting( + aperture = ThetaRepository.ApertureEnum.APERTURE_2_0, + colorTemperature = 6800, + exposureProgram = ThetaRepository.ExposureProgramEnum.MANUAL, + iso = ThetaRepository.IsoEnum.ISO_800, + shutterSpeed = ThetaRepository.ShutterSpeedEnum.SHUTTER_SPEED_ONE_OVER_100, + whiteBalance = ThetaRepository.WhiteBalanceEnum.CLOUDY_DAYLIGHT, + ) + ) + ) val bitrate = Pair("Fine", ThetaRepository.BitrateEnum.FINE) val bluetoothPower = Pair(BluetoothPower.ON, ThetaRepository.BluetoothPowerEnum.ON) val burstMode = Pair(BurstMode.ON, ThetaRepository.BurstModeEnum.ON) @@ -613,6 +711,7 @@ class OptionsTest { val orgOptions = ThetaRepository.Options( aiAutoThumbnail = aiAutoThumbnail.second, aperture = aperture.second, + autoBracket = autoBracket.second, bitrate = bitrate.second, bluetoothPower = bluetoothPower.second, burstMode = burstMode.second, @@ -670,6 +769,7 @@ class OptionsTest { assertEquals(options._aiAutoThumbnail, aiAutoThumbnail.first, "aiAutoThumbnail") assertEquals(options.aperture, aperture.first, "aperture") + assertEquals(options._autoBracket, autoBracket.first, "autoBracket") assertEquals(options._bitrate, bitrate.first, "bitrate") assertEquals(options._bluetoothPower, bluetoothPower.first, "bluetoothPower") assertEquals(options._burstMode, burstMode.first, "burstMode") diff --git a/kotlin-multiplatform/src/commonTest/resources/CompositeIntervalCapture/start_capture_cancel.json b/kotlin-multiplatform/src/commonTest/resources/CompositeIntervalCapture/start_capture_cancel.json new file mode 100644 index 0000000000..77e77d98bd --- /dev/null +++ b/kotlin-multiplatform/src/commonTest/resources/CompositeIntervalCapture/start_capture_cancel.json @@ -0,0 +1,8 @@ +{ + "error": { + "code": "canceledShooting", + "message": "shooting is canceled." + }, + "name": "camera.startCapture", + "state": "error" +} diff --git a/kotlin-multiplatform/src/commonTest/resources/CompositeIntervalCapture/start_capture_done.json b/kotlin-multiplatform/src/commonTest/resources/CompositeIntervalCapture/start_capture_done.json new file mode 100644 index 0000000000..31387e2444 --- /dev/null +++ b/kotlin-multiplatform/src/commonTest/resources/CompositeIntervalCapture/start_capture_done.json @@ -0,0 +1 @@ +{"results":{"fileUrls":["http://192.168.1.1/files/100RICOH/R0010026.JPG"]},"name":"camera.startCapture","state":"done"} \ No newline at end of file diff --git a/kotlin-multiplatform/src/commonTest/resources/CompositeIntervalCapture/start_capture_done_empty.json b/kotlin-multiplatform/src/commonTest/resources/CompositeIntervalCapture/start_capture_done_empty.json new file mode 100644 index 0000000000..d8c34e3908 --- /dev/null +++ b/kotlin-multiplatform/src/commonTest/resources/CompositeIntervalCapture/start_capture_done_empty.json @@ -0,0 +1 @@ +{"results":{"fileUrls":[]},"name":"camera.startCapture","state":"done"} \ No newline at end of file diff --git a/kotlin-multiplatform/src/commonTest/resources/CompositeIntervalCapture/start_capture_error.json b/kotlin-multiplatform/src/commonTest/resources/CompositeIntervalCapture/start_capture_error.json new file mode 100644 index 0000000000..177c574330 --- /dev/null +++ b/kotlin-multiplatform/src/commonTest/resources/CompositeIntervalCapture/start_capture_error.json @@ -0,0 +1,8 @@ +{ + "error": { + "code": "UnitTest", + "message": "UnitTest error" + }, + "name": "camera.startCapture", + "state": "error" +} diff --git a/kotlin-multiplatform/src/commonTest/resources/CompositeIntervalCapture/start_capture_progress.json b/kotlin-multiplatform/src/commonTest/resources/CompositeIntervalCapture/start_capture_progress.json new file mode 100644 index 0000000000..8818ce4691 --- /dev/null +++ b/kotlin-multiplatform/src/commonTest/resources/CompositeIntervalCapture/start_capture_progress.json @@ -0,0 +1 @@ +{"id":"2792","name":"camera.startCapture","progress":{"completion":0.00},"state":"inProgress"} diff --git a/kotlin-multiplatform/src/commonTest/resources/CompositeIntervalCapture/state_idle.json b/kotlin-multiplatform/src/commonTest/resources/CompositeIntervalCapture/state_idle.json new file mode 100644 index 0000000000..e9c43eff77 --- /dev/null +++ b/kotlin-multiplatform/src/commonTest/resources/CompositeIntervalCapture/state_idle.json @@ -0,0 +1,22 @@ +{ + "fingerprint": "FIG_0008", + "state": { + "_apiVersion": 2, + "batteryLevel": 0.81, + "_batteryState": "disconnect", + "_cameraError": [ + "COMPASS_CALIBRATION" + ], + "_captureStatus": "idle", + "_capturedPictures": 0, + "_compositeShootingElapsedTime": 0, + "_function": "selfTimer", + "_latestFileUrl": "http://192.168.1.1/files/150100524436344d4201375fda9dc400/100RICOH/R0013331.JPG", + "_mySettingChanged": false, + "_pluginRunning": false, + "_pluginWebServer": true, + "_recordableTime": 0, + "_recordedTime": 0, + "storageUri": "http://192.168.1.1/files/150100524436344d4201375fda9dc400/" + } +} diff --git a/kotlin-multiplatform/src/commonTest/resources/CompositeIntervalCapture/state_self_timer.json b/kotlin-multiplatform/src/commonTest/resources/CompositeIntervalCapture/state_self_timer.json new file mode 100644 index 0000000000..e34db1a80d --- /dev/null +++ b/kotlin-multiplatform/src/commonTest/resources/CompositeIntervalCapture/state_self_timer.json @@ -0,0 +1 @@ +{"fingerprint":"FIG_0003","state":{"batteryLevel":0.8,"storageUri":"http://192.168.1.1/files/thetasc26c21a247daf35838792bad9e","_apiVersion":2,"_batteryState":"charging","_cameraError":[],"_captureStatus":"idle","_capturedPictures":0,"_latestFileUrl":"http://192.168.1.1/files/thetasc26c21a247daf35838792bad9e/100RICOH/R0012313.JPG","_recordableTime":0,"_recordedTime":0,"_function":"selfTimer"}} diff --git a/kotlin-multiplatform/src/commonTest/resources/CompositeIntervalCapture/state_shooting.json b/kotlin-multiplatform/src/commonTest/resources/CompositeIntervalCapture/state_shooting.json new file mode 100644 index 0000000000..b6aecc5163 --- /dev/null +++ b/kotlin-multiplatform/src/commonTest/resources/CompositeIntervalCapture/state_shooting.json @@ -0,0 +1,22 @@ +{ + "fingerprint": "FIG_0008", + "state": { + "_apiVersion": 2, + "batteryLevel": 0.81, + "_batteryState": "disconnect", + "_cameraError": [ + "COMPASS_CALIBRATION" + ], + "_captureStatus": "shooting", + "_capturedPictures": 0, + "_compositeShootingElapsedTime": 0, + "_function": "selfTimer", + "_latestFileUrl": "http://192.168.1.1/files/150100524436344d4201375fda9dc400/100RICOH/R0013331.JPG", + "_mySettingChanged": false, + "_pluginRunning": false, + "_pluginWebServer": true, + "_recordableTime": 0, + "_recordedTime": 0, + "storageUri": "http://192.168.1.1/files/150100524436344d4201375fda9dc400/" + } +} diff --git a/kotlin-multiplatform/src/commonTest/resources/CompositeIntervalCapture/stop_capture_done.json b/kotlin-multiplatform/src/commonTest/resources/CompositeIntervalCapture/stop_capture_done.json new file mode 100644 index 0000000000..53f4b47b1c --- /dev/null +++ b/kotlin-multiplatform/src/commonTest/resources/CompositeIntervalCapture/stop_capture_done.json @@ -0,0 +1 @@ +{"name":"camera.stopCapture","state":"done"} \ No newline at end of file diff --git a/kotlin-multiplatform/src/commonTest/resources/CompositeIntervalCapture/stop_capture_error.json b/kotlin-multiplatform/src/commonTest/resources/CompositeIntervalCapture/stop_capture_error.json new file mode 100644 index 0000000000..0fa6f9358c --- /dev/null +++ b/kotlin-multiplatform/src/commonTest/resources/CompositeIntervalCapture/stop_capture_error.json @@ -0,0 +1,8 @@ +{ + "error": { + "code": "UnitTest", + "message": "UnitTest error" + }, + "name": "camera.stopCapture", + "state": "error" +} diff --git a/kotlin-multiplatform/src/commonTest/resources/options/option_auto_bracket.json b/kotlin-multiplatform/src/commonTest/resources/options/option_auto_bracket.json new file mode 100644 index 0000000000..545da84c46 --- /dev/null +++ b/kotlin-multiplatform/src/commonTest/resources/options/option_auto_bracket.json @@ -0,0 +1 @@ +{"name":"camera.getOptions","results":{"options":{"_autoBracket":{"_bracketNumber":2,"_bracketParameters":[{"_colorTemperature":5000,"exposureCompensation":0.0,"exposureProgram":1,"iso":400,"shutterSpeed":0.004,"whiteBalance":"auto"},{"_colorTemperature":5000,"exposureCompensation":0.0,"exposureProgram":1,"iso":400,"shutterSpeed":0.004,"whiteBalance":"auto"}]}}},"state":"done"} diff --git a/kotlin-multiplatform/src/iosMain/kotlin/com/ricoh360/thetaclient/Platform.kt b/kotlin-multiplatform/src/iosMain/kotlin/com/ricoh360/thetaclient/Platform.kt index 7b6f1f15a5..83596954e9 100644 --- a/kotlin-multiplatform/src/iosMain/kotlin/com/ricoh360/thetaclient/Platform.kt +++ b/kotlin-multiplatform/src/iosMain/kotlin/com/ricoh360/thetaclient/Platform.kt @@ -21,14 +21,15 @@ actual class Platform actual constructor() { actual typealias FrameSource = NSData /** fixed NSData */ -internal val data: NSMutableData = NSMutableData.create(length = 10)!! +internal val data: NSMutableData = NSMutableData.create(length = 10u)!! /** * convert [packet] to NSData frame source data */ +@OptIn(ExperimentalForeignApi::class) actual fun frameFrom(packet: Pair): FrameSource { packet.first.usePinned { - val range = NSMakeRange(0, data.length) + val range = NSMakeRange(0u, data.length) data.replaceBytesInRange( range = range, withBytes = it.addressOf(0), diff --git a/react-native/android/build.gradle b/react-native/android/build.gradle index f6d5ab0a00..4b5458715e 100644 --- a/react-native/android/build.gradle +++ b/react-native/android/build.gradle @@ -134,7 +134,7 @@ dependencies { //noinspection GradleDynamicVersion implementation "com.facebook.react:react-native:+" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4-native-mt" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4" implementation "com.ricoh360.thetaclient:theta-client:1.5.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 6cba9025d1..5dd7f2a6c7 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 @@ -8,12 +8,7 @@ import com.facebook.react.bridge.WritableMap import com.facebook.react.bridge.WritableArray import com.ricoh360.thetaclient.DigestAuth import com.ricoh360.thetaclient.ThetaRepository.* -import com.ricoh360.thetaclient.capture.Capture -import com.ricoh360.thetaclient.capture.PhotoCapture -import com.ricoh360.thetaclient.capture.TimeShiftCapture -import com.ricoh360.thetaclient.capture.VideoCapture -import com.ricoh360.thetaclient.capture.LimitlessIntervalCapture -import com.ricoh360.thetaclient.capture.ShotCountSpecifiedIntervalCapture +import com.ricoh360.thetaclient.capture.* const val KEY_NOTIFY_NAME = "name" const val KEY_NOTIFY_PARAMS = "params" @@ -23,6 +18,7 @@ const val KEY_NOTIFY_PARAM_MESSAGE = "message" val optionItemNameToEnum: Map = mutableMapOf( "aiAutoThumbnail" to OptionNameEnum.AiAutoThumbnail, "aperture" to OptionNameEnum.Aperture, + "autoBracket" to OptionNameEnum.AutoBracket, "bitrate" to OptionNameEnum.Bitrate, "bluetoothPower" to OptionNameEnum.BluetoothPower, "burstMode" to OptionNameEnum.BurstMode, @@ -51,6 +47,7 @@ val optionItemNameToEnum: Map = mutableMapOf( "iso" to OptionNameEnum.Iso, "isoAutoHighLimit" to OptionNameEnum.IsoAutoHighLimit, "language" to OptionNameEnum.Language, + "latestEnabledExposureDelayTime" to OptionNameEnum.LatestEnabledExposureDelayTime, "maxRecordableTime" to OptionNameEnum.MaxRecordableTime, "networkType" to OptionNameEnum.NetworkType, "offDelay" to OptionNameEnum.OffDelay, @@ -69,6 +66,8 @@ val optionItemNameToEnum: Map = mutableMapOf( "timeShift" to OptionNameEnum.TimeShift, "totalSpace" to OptionNameEnum.TotalSpace, "username" to OptionNameEnum.Username, + "videoStitching" to OptionNameEnum.VideoStitching, + "visibilityReduction" to OptionNameEnum.VisibilityReduction, "whiteBalance" to OptionNameEnum.WhiteBalance, "whiteBalanceAutoStrength" to OptionNameEnum.WhiteBalanceAutoStrength, "wlanFrequency" to OptionNameEnum.WlanFrequency, @@ -202,6 +201,18 @@ fun setShotCountSpecifiedIntervalCaptureBuilderParams(optionMap: ReadableMap, bu } } +fun setCompositeIntervalCaptureBuilderParams(optionMap: ReadableMap, builder: CompositeIntervalCapture.Builder) { + val interval = if (optionMap.hasKey("_capture_interval")) optionMap.getInt("_capture_interval") else null + interval?.let { + if (it >= 0) { + builder.setCheckStatusCommandInterval(it.toLong()) + } + } + if (optionMap.hasKey("compositeShootingOutputInterval")) { + builder.setCompositeShootingOutputInterval(optionMap.getInt("compositeShootingOutputInterval")) + } +} + fun toGetOptionsParam(optionNames: ReadableArray): MutableList { val optionNameList = mutableListOf() for (index in 0..(optionNames.size() - 1)) { @@ -231,7 +242,11 @@ fun toResult(options: Options): WritableMap { OptionNameEnum.Username ) OptionNameEnum.values().forEach { name -> - if (name == OptionNameEnum.Bitrate) { + if (name == OptionNameEnum.AutoBracket) { + options.autoBracket?.let { + result.putArray("autoBracket", toResult(autoBracket = it)) + } + } else if (name == OptionNameEnum.Bitrate) { options.bitrate?.let { bitrate -> if (bitrate is BitrateEnum) { result.putString("bitrate", bitrate.toString()) @@ -288,6 +303,38 @@ fun addOptionsValueToMap(options: Options, name: OptionNameEnum, objects: Wr } } +fun toResult(autoBracket: BracketSettingList): WritableArray { + val resultList = Arguments.createArray() + + autoBracket.list?.forEach { bracketSetting -> + val result = Arguments.createMap() + bracketSetting.aperture?.name?.let { name -> + result.putString("aperture", name) + } + bracketSetting.colorTemperature?.let { value -> + result.putInt("colorTemperature", value) + } + bracketSetting.exposureCompensation?.name?.let { name -> + result.putString("exposureCompensation", name) + } + bracketSetting.exposureProgram?.name?.let { name -> + result.putString("exposureProgram", name) + } + bracketSetting.iso?.name?.let { name -> + result.putString("iso", name) + } + bracketSetting.shutterSpeed?.name?.let { name -> + result.putString("shutterSpeed", name) + } + bracketSetting.whiteBalance?.name?.let { name -> + result.putString("whiteBalance", name) + } + resultList.pushMap(result) + } + + return resultList +} + fun toResult(burstOption: BurstOption): WritableMap { val result = Arguments.createMap() burstOption.burstCaptureNum?.value?.name?.let { name -> @@ -463,6 +510,10 @@ fun setOptionValue(options: Options, name: OptionNameEnum, optionsMap: ReadableM options.setValue(name, optionsMap.getDouble(key).toLong()) } else if (boolOptions.contains(name)) { options.setValue(name, optionsMap.getBoolean(key)) + } else if (name == OptionNameEnum.AutoBracket) { + optionsMap.getArray(key)?.let { + options.setValue(name, toAutoBracket(list = it)) + } } else if (name == OptionNameEnum.Bitrate) { val type = optionsMap.getType(key) if (type == ReadableType.Number) { @@ -519,6 +570,7 @@ fun getOptionValueEnum(name: OptionNameEnum, valueName: String): Any? { OptionNameEnum.Iso -> IsoEnum.values().find { it.name == valueName } OptionNameEnum.IsoAutoHighLimit -> IsoAutoHighLimitEnum.values().find { it.name == valueName } OptionNameEnum.Language -> LanguageEnum.values().find { it.name == valueName } + OptionNameEnum.LatestEnabledExposureDelayTime -> ExposureDelayEnum.values().find { it.name == valueName } OptionNameEnum.MaxRecordableTime -> MaxRecordableTimeEnum.values().find { it.name == valueName } OptionNameEnum.NetworkType -> NetworkTypeEnum.values().find { it.name == valueName } OptionNameEnum.OffDelay -> OffDelayEnum.values().find { it.name == valueName } @@ -528,6 +580,8 @@ fun getOptionValueEnum(name: OptionNameEnum, valueName: String): Any? { OptionNameEnum.ShootingMethod -> ShootingMethodEnum.values().find { it.name == valueName } OptionNameEnum.ShutterSpeed -> ShutterSpeedEnum.values().find { it.name == valueName } OptionNameEnum.SleepDelay -> SleepDelayEnum.values().find { it.name == valueName } + OptionNameEnum.VideoStitching -> VideoStitchingEnum.values().find { it.name == valueName } + OptionNameEnum.VisibilityReduction -> VisibilityReductionEnum.values().find { it.name == valueName } OptionNameEnum.WhiteBalance -> WhiteBalanceEnum.values().find { it.name == valueName } OptionNameEnum.WhiteBalanceAutoStrength -> WhiteBalanceAutoStrengthEnum.values().find { it.name == valueName } OptionNameEnum.WlanFrequency -> WlanFrequencyEnum.values().find { it.name == valueName } @@ -556,6 +610,26 @@ fun toListAccessPointsResult(accessPointList: List): WritableArray return result } +fun toAutoBracket(list: ReadableArray): BracketSettingList { + val autoBracket = BracketSettingList() + for(i in 0 until list.size()) { + list.getMap(i)?.let { setting -> + autoBracket.add( + BracketSetting( + aperture = setting.getString("aperture")?.let { ApertureEnum.valueOf(it) }, + colorTemperature = if (setting.hasKey("colorTemperature")) setting.getInt("colorTemperature") else null, + exposureCompensation = setting.getString("exposureCompensation")?.let { ExposureCompensationEnum.valueOf(it) }, + exposureProgram = setting.getString("exposureProgram")?.let { ExposureProgramEnum.valueOf(it) }, + iso = setting.getString("iso")?.let { IsoEnum.valueOf(it) }, + shutterSpeed = setting.getString("shutterSpeed")?.let { ShutterSpeedEnum.valueOf(it) }, + whiteBalance = setting.getString("whiteBalance")?.let { WhiteBalanceEnum.valueOf(it) }, + ) + ) + } + } + return autoBracket +} + fun toProxy(map: ReadableMap?): Proxy? { return map?.let { Proxy( 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 1929364692..9b5a690e1c 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 @@ -34,6 +34,9 @@ class ThetaClientReactNativeModule( var shotCountSpecifiedIntervalCaptureBuilder: ShotCountSpecifiedIntervalCapture.Builder? = null var shotCountSpecifiedIntervalCapture: ShotCountSpecifiedIntervalCapture? = null var shotCountSpecifiedIntervalCapturing: ShotCountSpecifiedIntervalCapturing? = null + var compositeIntervalCaptureBuilder: CompositeIntervalCapture.Builder? = null + var compositeIntervalCapture: CompositeIntervalCapture? = null + var compositeIntervalCapturing: CompositeIntervalCapturing? = null var theta: ThetaRepository? = null var listenerCount: Int = 0 @@ -96,6 +99,9 @@ class ThetaClientReactNativeModule( shotCountSpecifiedIntervalCaptureBuilder = null shotCountSpecifiedIntervalCapture = null shotCountSpecifiedIntervalCapturing = null + compositeIntervalCaptureBuilder = null + compositeIntervalCapture = null + compositeIntervalCapturing = null theta = ThetaRepository.newInstance( endpoint, @@ -594,7 +600,7 @@ class ThetaClientReactNativeModule( return } class StartCaptureCallback : TimeShiftCapture.StartCaptureCallback { - override fun onSuccess(fileUrl: String?) { + override fun onCaptureCompleted(fileUrl: String?) { promise.resolve(fileUrl) timeShiftCapture = null } @@ -605,10 +611,19 @@ class ThetaClientReactNativeModule( ) } - override fun onError(exception: ThetaRepository.ThetaRepositoryException) { + override fun onCaptureFailed(exception: ThetaRepository.ThetaRepositoryException) { promise.reject(exception) timeShiftCapture = null } + + override fun onStopFailed(exception: ThetaRepository.ThetaRepositoryException) { + sendNotifyEvent( + toNotify( + NOTIFY_TIMESHIFT_STOP_ERROR, + toMessageNotifyParam(exception.message ?: exception.toString()) + ) + ) + } } timeShiftCapturing = timeShiftCapture?.startCapture(StartCaptureCallback()) } @@ -937,6 +952,119 @@ class ThetaClientReactNativeModule( promise.resolve(true) } + /** + * getCompositeIntervalCaptureBuilder - get interval composite shooting builder from repository + * @param shootingTimeSec Shooting time for interval composite shooting (sec) + * @param promise Promise for getCompositeIntervalCaptureBuilder + */ + @ReactMethod + fun getCompositeIntervalCaptureBuilder(shootingTimeSec: Int, promise: Promise) { + val theta = theta + if (theta == null) { + promise.reject(Exception(messageNotInit)) + return + } + compositeIntervalCaptureBuilder = theta.getCompositeIntervalCaptureBuilder(shootingTimeSec) + promise.resolve(true) + } + + /** + * buildCompositeIntervalCapture - build interval composite shooting + * @param options option to execute interval shooting with the shot count specified + * @param promise Promise for buildCompositeIntervalCapture + */ + @ReactMethod + fun buildCompositeIntervalCapture(options: ReadableMap, promise: Promise) { + if (theta == null) { + promise.reject(Exception(messageNotInit)) + return + } + if (compositeIntervalCaptureBuilder == null) { + promise.reject(Exception("no compositeIntervalCaptureBuilder")) + return + } + launch { + try { + compositeIntervalCaptureBuilder?.let { + setCaptureBuilderParams(optionMap = options, builder = it) + setCompositeIntervalCaptureBuilderParams(optionMap = options, builder = it) + compositeIntervalCapture = it.build() + } + promise.resolve(true) + compositeIntervalCaptureBuilder = null + } catch (t: Throwable) { + promise.reject(t) + compositeIntervalCaptureBuilder = null + } + } + } + + /** + * startCompositeIntervalCapture - start interval composite shooting + * @param promise promise for startCompositeIntervalCapture + */ + @ReactMethod + fun startCompositeIntervalCapture(promise: Promise) { + if (theta == null) { + promise.reject(Exception(messageNotInit)) + return + } + if (compositeIntervalCapture == null) { + promise.reject(Exception("no compositeIntervalCapture")) + return + } + class StartCaptureCallback : CompositeIntervalCapture.StartCaptureCallback { + override fun onCaptureCompleted(fileUrls: List?) { + promise.resolve(fileUrls?.let { + val resultList = Arguments.createArray() + it.forEach { + resultList.pushString(it) + } + resultList + }) + compositeIntervalCapture = null + } + + override fun onProgress(completion: Float) { + sendNotifyEvent( + toNotify( + NOTIFY_COMPOSITE_INTERVAL_PROGRESS, + toCaptureProgressNotifyParam(value = completion) + ) + ) + } + + override fun onStopFailed(exception: ThetaRepository.ThetaRepositoryException) { + sendNotifyEvent( + toNotify( + NOTIFY_COMPOSITE_INTERVAL_STOP_ERROR, + toMessageNotifyParam(exception.message ?: exception.toString()) + ) + ) + } + + override fun onCaptureFailed(exception: ThetaRepository.ThetaRepositoryException) { + promise.reject(exception) + compositeIntervalCapture = null + } + } + compositeIntervalCapturing = compositeIntervalCapture?.startCapture(StartCaptureCallback()) + } + + /** + * cancelCompositeIntervalCapture - stop interval composite shooting + * @param promise promise for cancelCompositeIntervalCapture + */ + @ReactMethod + fun cancelCompositeIntervalCapture(promise: Promise) { + if (theta == null) { + promise.reject(Exception(messageNotInit)) + return + } + compositeIntervalCapturing?.cancelCapture() + promise.resolve(true) + } + /** * getMetadata - retrieve meta data from THETA via repository * @param promise promise to set result @@ -1545,9 +1673,12 @@ class ThetaClientReactNativeModule( const val EVENT_NAME = "ThetaFrameEvent" const val EVENT_NOTIFY = "ThetaNotify" const val NOTIFY_TIMESHIFT_PROGRESS = "TIME-SHIFT-PROGRESS" + const val NOTIFY_TIMESHIFT_STOP_ERROR = "TIME-SHIFT-STOP-ERROR" const val NOTIFY_VIDEO_CAPTURE_STOP_ERROR = "VIDEO-CAPTURE-STOP-ERROR" const val NOTIFY_LIMITLESS_INTERVAL_CAPTURE_STOP_ERROR = "LIMITLESS-INTERVAL-CAPTURE-STOP-ERROR" const val NOTIFY_SHOT_COUNT_SPECIFIED_INTERVAL_PROGRESS = "SHOT-COUNT-SPECIFIED-INTERVAL-PROGRESS" const val NOTIFY_SHOT_COUNT_SPECIFIED_INTERVAL_STOP_ERROR = "SHOT-COUNT-SPECIFIED-INTERVAL-STOP-ERROR" + const val NOTIFY_COMPOSITE_INTERVAL_PROGRESS = "COMPOSITE-INTERVAL-PROGRESS" + const val NOTIFY_COMPOSITE_INTERVAL_STOP_ERROR = "COMPOSITE-INTERVAL-STOP-ERROR" } } diff --git a/react-native/ios/ConvertUtil.swift b/react-native/ios/ConvertUtil.swift index d280840854..5c757b8603 100644 --- a/react-native/ios/ConvertUtil.swift +++ b/react-native/ios/ConvertUtil.swift @@ -19,6 +19,7 @@ let KEY_THETA_MODEL = "thetaModel" let KEY_TIMESHIFT = "timeShift" let KEY_APERTURE = "aperture" let KEY_CAPTURE_INTERVAL = "captureInterval" +let KEY_COMPOSITE_SHOOTING_OUTPUT_INTERVAL = "compositeShootingOutputInterval" let KEY_COLOR_TEMPERATURE = "colorTemperature" let KEY_EXPOSURE_COMPENSATION = "exposureCompensation" let KEY_EXPOSURE_DELAY = "exposureDelay" @@ -65,6 +66,7 @@ let optionItemNameToEnum = [ // TODO: Add items when adding options "aiAutoThumbnail": ThetaRepository.OptionNameEnum.aiautothumbnail, "aperture": ThetaRepository.OptionNameEnum.aperture, + "autoBracket": ThetaRepository.OptionNameEnum.autobracket, "bitrate": ThetaRepository.OptionNameEnum.bitrate, "bluetoothPower": ThetaRepository.OptionNameEnum.bluetoothpower, "burstMode": ThetaRepository.OptionNameEnum.burstmode, @@ -162,6 +164,10 @@ func setOptionsValue(options: ThetaRepository.Options, name: String, value: Any) options.aperture = getEnumValue( values: ThetaRepository.ApertureEnum.values(), name: value as! String )! + case ThetaRepository.OptionNameEnum.autobracket.name: + if let params = value as? [[String: Any]] { + options.autoBracket = toAutoBracket(params: params) + } case ThetaRepository.OptionNameEnum.bitrate.name: options.bitrate = toBitrate(value: value) case ThetaRepository.OptionNameEnum.bluetoothpower.name: @@ -360,6 +366,10 @@ func convertResult(options: ThetaRepository.Options) -> [String: Any] { result[key] = value as! Bool ? true : false } else if value is NSNumber || value is String { result[key] = value + } else if value is ThetaRepository.BracketSettingList, + let autoBracket = value as? ThetaRepository.BracketSettingList + { + result[key] = convertResult(autoBracket: autoBracket) } else if let bitrate = value as? ThetaRepository.BitrateNumber { result[key] = bitrate.value } else if value is ThetaRepository.BurstOption, @@ -558,6 +568,17 @@ func setShotCountSpecifiedIntervalCaptureBuilderParams(params: [String: Any], bu } } +func setCompositeIntervalCaptureBuilderParams(params: [String: Any], builder: CompositeIntervalCapture.Builder) { + if let interval = params[KEY_TIMESHIFT_CAPTURE_INTERVAL] as? Int, + interval >= 0 + { + builder.setCheckStatusCommandInterval(timeMillis: Int64(interval)) + } + if let value = params[KEY_COMPOSITE_SHOOTING_OUTPUT_INTERVAL] as? Int32 { + builder.setCompositeShootingOutputInterval(sec: value) + } +} + // MARK: - Utility func getEnumValue>(values: KotlinArray, name: String) -> E? { @@ -742,6 +763,40 @@ func convertResult(burstOption: ThetaRepository.BurstOption) -> [String: Any] { return result } +func convertResult(autoBracket: ThetaRepository.BracketSettingList) -> [[String: Any]] { + var resultList = [[String: Any]]() + + autoBracket.list.forEach { bracketSetting in + var result: [String: Any] = [:] + if let setting = bracketSetting as? ThetaRepository.BracketSetting { + if let aperture = setting.aperture?.name { + result["aperture"] = aperture + } + if let colorTemperature = setting.colorTemperature?.intValue { + result["colorTemperature"] = colorTemperature + } + if let exposureCompensation = setting.exposureCompensation?.name { + result["exposureCompensation"] = exposureCompensation + } + if let exposureProgram = setting.exposureProgram?.name { + result["exposureProgram"] = exposureProgram + } + if let iso = setting.iso?.name { + result["iso"] = iso + } + if let shutterSpeed = setting.shutterSpeed?.name { + result["shutterSpeed"] = shutterSpeed + } + if let whiteBalance = setting.whiteBalance?.name { + result["whiteBalance"] = whiteBalance + } + resultList.append(result) + } + } + + return resultList +} + func convertResult(gpsInfo: ThetaRepository.GpsInfo) -> [String: Any] { return [ "latitude": gpsInfo.latitude, @@ -941,6 +996,80 @@ func toBurstOption(params: [String: Any]) -> ThetaRepository.BurstOption { ) } +func toAutoBracket(params: [[String: Any]]) -> ThetaRepository.BracketSettingList { + let autoBracket = ThetaRepository.BracketSettingList(list: NSMutableArray()) + + params.forEach { map in + let aperture = { + if let name = map["aperture"] as? String { + return getEnumValue(values: ThetaRepository.ApertureEnum.values(), name: name) + } else { + return nil + } + }() + + let colorTemperature = { + if let value = map["colorTemperature"] as? Int { + return toKotlinInt(value: value) + } else { + return nil + } + }() + + let exposureCompensation = { + if let name = map["exposureCompensation"] as? String { + return getEnumValue(values: ThetaRepository.ExposureCompensationEnum.values(), name: name) + } else { + return nil + } + }() + + let exposureProgram = { + if let name = map["exposureProgram"] as? String { + return getEnumValue(values: ThetaRepository.ExposureProgramEnum.values(), name: name) + } else { + return nil + } + }() + + let iso = { + if let name = map["iso"] as? String { + return getEnumValue(values: ThetaRepository.IsoEnum.values(), name: name) + } else { + return nil + } + }() + + let shutterSpeed = { + if let name = map["shutterSpeed"] as? String { + return getEnumValue(values: ThetaRepository.ShutterSpeedEnum.values(), name: name) + } else { + return nil + } + }() + + let whiteBalance = { + if let name = map["whiteBalance"] as? String { + return getEnumValue(values: ThetaRepository.WhiteBalanceEnum.values(), name: name) + } else { + return nil + } + }() + + autoBracket.add(setting: ThetaRepository.BracketSetting( + aperture: aperture, + colorTemperature: colorTemperature, + exposureCompensation: exposureCompensation, + exposureProgram: exposureProgram, + iso: iso, + shutterSpeed: shutterSpeed, + whiteBalance: whiteBalance + )) + } + + return autoBracket +} + func toGpsInfo(params: [String: Any]) -> ThetaRepository.GpsInfo? { guard let latitude = params[KEY_GPS_LATITUDE] as? Double, let longitude = params[KEY_GPS_LONGITUDE] as? Double, diff --git a/react-native/ios/ThetaClientReactNative.m b/react-native/ios/ThetaClientReactNative.m index c3be4fc8bf..2201b46c02 100644 --- a/react-native/ios/ThetaClientReactNative.m +++ b/react-native/ios/ThetaClientReactNative.m @@ -121,6 +121,20 @@ @interface RCT_EXTERN_MODULE(ThetaClientReactNative, NSObject) RCT_EXTERN_METHOD(cancelShotCountSpecifiedIntervalCapture:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject) +RCT_EXTERN_METHOD(getCompositeIntervalCaptureBuilder:(int)shootingTimeSec + withResolver:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) + +RCT_EXTERN_METHOD(buildCompositeIntervalCapture:(NSDictionary*)options + withResolver:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) + +RCT_EXTERN_METHOD(startCompositeIntervalCapture:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) + +RCT_EXTERN_METHOD(cancelCompositeIntervalCapture:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) + RCT_EXTERN_METHOD(getMetadata:(NSString*)fileUrl withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject) diff --git a/react-native/ios/ThetaClientReactNative.swift b/react-native/ios/ThetaClientReactNative.swift index 9d7be7dd5a..372ce707dd 100644 --- a/react-native/ios/ThetaClientReactNative.swift +++ b/react-native/ios/ThetaClientReactNative.swift @@ -18,6 +18,9 @@ let MESSAGE_NO_LIMITLESS_INTERVAL_CAPTURING = "no limitlessIntervalCapturing." let MESSAGE_NO_SHOT_COUNT_SPECIFIED_INTERVAL_CAPTURE = "No shotCountSpecifiedIntervalCapture." let MESSAGE_NO_SHOT_COUNT_SPECIFIED_INTERVAL_CAPTURE_BUILDER = "no shotCountSpecifiedIntervalCaptureBuilder." let MESSAGE_NO_SHOT_COUNT_SPECIFIED_INTERVAL_CAPTURING = "no shotCountSpecifiedIntervalCapturing." +let MESSAGE_NO_COMPOSITE_INTERVAL_CAPTURE = "No compositeIntervalCapture." +let MESSAGE_NO_COMPOSITE_INTERVAL_CAPTURE_BUILDER = "no compositeIntervalCaptureBuilder." +let MESSAGE_NO_COMPOSITE_INTERVAL_CAPTURING = "no compositeIntervalCapturing." @objc(ThetaClientReactNative) class ThetaClientReactNative: RCTEventEmitter { @@ -37,16 +40,22 @@ class ThetaClientReactNative: RCTEventEmitter { var shotCountSpecifiedIntervalCaptureBuilder: ShotCountSpecifiedIntervalCapture.Builder? var shotCountSpecifiedIntervalCapture: ShotCountSpecifiedIntervalCapture? var shotCountSpecifiedIntervalCapturing: ShotCountSpecifiedIntervalCapturing? + var compositeIntervalCaptureBuilder: CompositeIntervalCapture.Builder? + var compositeIntervalCapture: CompositeIntervalCapture? + var compositeIntervalCapturing: CompositeIntervalCapturing? static let EVENT_FRAME = "ThetaFrameEvent" static let EVENT_NOTIFY = "ThetaNotify" static let NOTIFY_TIMESHIFT_PROGRESS = "TIME-SHIFT-PROGRESS" + static let NOTIFY_TIMESHIFT_STOP_ERROR = "TIME-SHIFT-STOP-ERROR" static let NOTIFY_SHOT_COUNT_SPECIFIED_INTERVAL_PROGRESS = "SHOT-COUNT-SPECIFIED-INTERVAL-PROGRESS" static let NOTIFY_SHOT_COUNT_SPECIFIED_INTERVAL_STOP_ERROR = "SHOT-COUNT-SPECIFIED-INTERVAL-STOP-ERROR" static let NOTIFY_VIDEO_CAPTURE_STOP_ERROR = "VIDEO-CAPTURE-STOP-ERROR" static let NOTIFY_LIMITLESS_INTERVAL_CAPTURE_STOP_ERROR = "LIMITLESS-INTERVAL-CAPTURE-STOP-ERROR" - + static let NOTIFY_COMPOSITE_INTERVAL_PROGRESS = "COMPOSITE-INTERVAL-PROGRESS" + static let NOTIFY_COMPOSITE_INTERVAL_STOP_ERROR = "COMPOSITE-INTERVAL-STOP-ERROR" + @objc override func supportedEvents() -> [String]! { return [ThetaClientReactNative.EVENT_FRAME, ThetaClientReactNative.EVENT_NOTIFY] @@ -87,6 +96,9 @@ class ThetaClientReactNative: RCTEventEmitter { shotCountSpecifiedIntervalCaptureBuilder = nil shotCountSpecifiedIntervalCapture = nil shotCountSpecifiedIntervalCapturing = nil + compositeIntervalCaptureBuilder = nil + compositeIntervalCapture = nil + compositeIntervalCapturing = nil previewing = false Task { @@ -584,10 +596,21 @@ class ThetaClientReactNative: RCTEventEmitter { self.client = client } - func onError(exception: ThetaRepository.ThetaRepositoryException) { + func onCaptureFailed(exception: ThetaRepository.ThetaRepositoryException) { callback(nil, exception.asError()) } + func onStopFailed(exception: ThetaRepository.ThetaRepositoryException) { + let error = exception.asError() + client?.sendEvent( + withName: ThetaClientReactNative.EVENT_NOTIFY, + body: toNotify( + name: ThetaClientReactNative.NOTIFY_TIMESHIFT_STOP_ERROR, + params: toMessageNotifyParam(value: error.localizedDescription) + ) + ) + } + func onProgress(completion: Float) { client?.sendEvent( withName: ThetaClientReactNative.EVENT_NOTIFY, @@ -598,7 +621,7 @@ class ThetaClientReactNative: RCTEventEmitter { ) } - func onSuccess(fileUrl: String?) { + func onCaptureCompleted(fileUrl: String?) { callback(fileUrl, nil) } } @@ -1000,6 +1023,136 @@ class ThetaClientReactNative: RCTEventEmitter { shotCountSpecifiedIntervalCapturing.cancelCapture() resolve(nil) } + + @objc(getCompositeIntervalCaptureBuilder:withResolver:withRejecter:) + func getCompositeIntervalCaptureBuilder( + shootingTimeSec: Int, + resolve: RCTPromiseResolveBlock, + reject: RCTPromiseRejectBlock + ) { + guard let thetaRepository else { + reject(ERROR_CODE_ERROR, MESSAGE_NOT_INIT, nil) + return + } + compositeIntervalCaptureBuilder = thetaRepository.getCompositeIntervalCaptureBuilder(shootingTimeSec: Int32(shootingTimeSec)) + resolve(nil) + } + + @objc(buildCompositeIntervalCapture:withResolver:withRejecter:) + func buildCompositeIntervalCapture( + options: [AnyHashable: Any]?, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + guard let _ = thetaRepository else { + reject(ERROR_CODE_ERROR, MESSAGE_NOT_INIT, nil) + return + } + guard let compositeIntervalCaptureBuilder else { + reject(ERROR_CODE_ERROR, MESSAGE_NO_COMPOSITE_INTERVAL_CAPTURE_BUILDER, nil) + return + } + + if let options = options as? [String: Any] { + setCaptureBuilderParams(params: options, builder: compositeIntervalCaptureBuilder) + setCompositeIntervalCaptureBuilderParams(params: options, builder: compositeIntervalCaptureBuilder) + } + compositeIntervalCaptureBuilder.build { capture, error in + if let error { + reject(ERROR_CODE_ERROR, error.localizedDescription, error) + } else if let capture { + self.compositeIntervalCapture = capture + self.compositeIntervalCaptureBuilder = nil + resolve(true) + } else { + reject(ERROR_CODE_ERROR, MESSAGE_NO_COMPOSITE_INTERVAL_CAPTURE, nil) + } + } + } + + @objc(startCompositeIntervalCapture:withRejecter:) + func startCompositeIntervalCapture( + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + guard let _ = thetaRepository else { + reject(ERROR_CODE_ERROR, MESSAGE_NOT_INIT, nil) + return + } + guard let compositeIntervalCapture else { + reject(ERROR_CODE_ERROR, MESSAGE_NO_COMPOSITE_INTERVAL_CAPTURE, nil) + return + } + + class Callback: CompositeIntervalCaptureStartCaptureCallback { + let callback: (_ urls: [String]?, _ error: Error?) -> Void + weak var client: ThetaClientReactNative? + init( + _ callback: @escaping (_ urls: [String]?, _ error: Error?) -> Void, + client: ThetaClientReactNative + ) { + self.callback = callback + self.client = client + } + + func onCaptureFailed(exception: ThetaRepository.ThetaRepositoryException) { + callback(nil, exception.asError()) + } + + func onProgress(completion: Float) { + client?.sendEvent( + withName: ThetaClientReactNative.EVENT_NOTIFY, + body: toNotify( + name: ThetaClientReactNative.NOTIFY_COMPOSITE_INTERVAL_PROGRESS, + params: toCaptureProgressNotifyParam(value: completion) + ) + ) + } + + func onCaptureCompleted(fileUrls: [String]?) { + callback(fileUrls, nil) + } + + func onStopFailed(exception: ThetaRepository.ThetaRepositoryException) { + let error = exception.asError() + client?.sendEvent( + withName: ThetaClientReactNative.EVENT_NOTIFY, + body: toNotify( + name: ThetaClientReactNative.NOTIFY_COMPOSITE_INTERVAL_STOP_ERROR, + params: toMessageNotifyParam(value: error.localizedDescription) + ) + ) + } + } + + compositeIntervalCapturing = compositeIntervalCapture.startCapture( + callback: Callback( + { url, error in + if let error { + reject(ERROR_CODE_ERROR, error.localizedDescription, error) + } else { + resolve(url) + } + }, client: self + )) + } + + @objc(cancelCompositeIntervalCapture:withRejecter:) + func cancelCompositeIntervalCapture( + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + guard let _ = thetaRepository else { + reject(ERROR_CODE_ERROR, MESSAGE_NOT_INIT, nil) + return + } + guard let compositeIntervalCapturing else { + reject(ERROR_CODE_ERROR, MESSAGE_NO_COMPOSITE_INTERVAL_CAPTURING, nil) + return + } + compositeIntervalCapturing.cancelCapture() + resolve(nil) + } @objc(getMetadata:withResolver:withRejecter:) func getMetadata( diff --git a/react-native/package.json b/react-native/package.json index 8ed9b09748..299625f850 100644 --- a/react-native/package.json +++ b/react-native/package.json @@ -68,7 +68,7 @@ "pod-install": "^0.1.0", "prettier": "^2.8.1", "react": "18.2.0", - "react-native": "0.71.7", + "react-native": "0.71.14", "react-native-builder-bob": "^0.20.3", "release-it": "^15.5.1", "typescript": "^4.9.4" diff --git a/react-native/src/__mocks__/react-native.ts b/react-native/src/__mocks__/react-native.ts index 75ea20e162..744ad4beb9 100644 --- a/react-native/src/__mocks__/react-native.ts +++ b/react-native/src/__mocks__/react-native.ts @@ -22,6 +22,10 @@ export const NativeModules = { buildShotCountSpecifiedIntervalCapture: jest.fn(), startShotCountSpecifiedIntervalCapture: jest.fn(), cancelShotCountSpecifiedIntervalCapture: jest.fn(), + getCompositeIntervalCaptureBuilder: jest.fn(), + buildCompositeIntervalCapture: jest.fn(), + startCompositeIntervalCapture: jest.fn(), + cancelCompositeIntervalCapture: jest.fn(), }, }; diff --git a/react-native/src/__tests__/capture/composite-interval-capture.test.ts b/react-native/src/__tests__/capture/composite-interval-capture.test.ts new file mode 100644 index 0000000000..264b3f246e --- /dev/null +++ b/react-native/src/__tests__/capture/composite-interval-capture.test.ts @@ -0,0 +1,288 @@ +import { NativeModules } from 'react-native'; +import { + getCompositeIntervalCaptureBuilder, + initialize, +} from '../../theta-repository'; +import { + BaseNotify, + NotifyController, +} from '../../theta-repository/notify-controller'; +import { NativeEventEmitter_addListener } from '../../__mocks__/react-native'; + +describe('interval composite shooting', () => { + const thetaClient = NativeModules.ThetaClientReactNative; + + beforeEach(() => { + jest.clearAllMocks(); + NotifyController.instance.release(); + }); + + afterEach(() => { + thetaClient.initialize = jest.fn(); + thetaClient.buildCompositeIntervalCapture = jest.fn(); + thetaClient.startCompositeIntervalCapture = jest.fn(); + thetaClient.cancelCompositeIntervalCapture = jest.fn(); + NotifyController.instance.release(); + }); + + test('getCompositeIntervalCaptureBuilder', async () => { + jest.mocked(NativeEventEmitter_addListener).mockImplementation( + jest.fn(() => { + return { + remove: jest.fn(), + }; + }) + ); + const builder = getCompositeIntervalCaptureBuilder(600); + expect(builder.interval).toBeUndefined(); + + builder.setCheckStatusCommandInterval(1); + builder.setCompositeShootingOutputInterval(60); + + expect(builder.interval).toBe(1); + expect(builder.options.compositeShootingOutputInterval).toBe(60); + + let isCallBuild = false; + jest.mocked(thetaClient.buildCompositeIntervalCapture).mockImplementation( + jest.fn(async (options) => { + expect(options._capture_interval).toBe(1); + expect(options.compositeShootingOutputInterval).toBe(60); + isCallBuild = true; + }) + ); + + const capture = await builder.build(); + expect(capture).toBeDefined(); + expect(capture.notify).toBeDefined(); + expect(isCallBuild).toBeTruthy(); + + expect(thetaClient.getCompositeIntervalCaptureBuilder).toHaveBeenCalledWith( + 600 + ); + }); + + test('build no interval', async () => { + jest.mocked(NativeEventEmitter_addListener).mockImplementation( + jest.fn(() => { + return { + remove: jest.fn(), + }; + }) + ); + const builder = getCompositeIntervalCaptureBuilder(600); + expect(builder.interval).toBeUndefined(); + + jest.mocked(thetaClient.buildCompositeIntervalCapture).mockImplementation( + jest.fn(async (options) => { + expect(options._capture_interval).toBe(-1); + }) + ); + + const capture = await builder.build(); + expect(capture).toBeDefined(); + }); + + test('startCapture', async () => { + jest.mocked(NativeEventEmitter_addListener).mockImplementation( + jest.fn(() => { + return { + remove: jest.fn(), + }; + }) + ); + await initialize(); + const builder = getCompositeIntervalCaptureBuilder(600); + jest + .mocked(thetaClient.buildCompositeIntervalCapture) + .mockImplementation(jest.fn(async () => {})); + const testUrls = ['http://192.168.1.1/files/100RICOH/R100.JPG']; + jest.mocked(thetaClient.startCompositeIntervalCapture).mockImplementation( + jest.fn(async () => { + return testUrls; + }) + ); + + const capture = await builder.build(); + const fileUrls = await capture.startCapture(); + expect(fileUrls).toBe(testUrls); + expect(NotifyController.instance.notifyList.size).toBe(0); + }); + + test('cancelCapture', (done) => { + jest.mocked(NativeEventEmitter_addListener).mockImplementation( + jest.fn(() => { + return { + remove: jest.fn(), + }; + }) + ); + const builder = getCompositeIntervalCaptureBuilder(600); + jest + .mocked(thetaClient.buildCompositeIntervalCapture) + .mockImplementation(jest.fn(async () => {})); + jest.mocked(thetaClient.startCompositeIntervalCapture).mockImplementation( + jest.fn(async () => { + return null; + }) + ); + + builder.build().then((capture) => { + capture.startCapture().then((value) => { + expect(value).toBeUndefined(); + done(); + }); + capture.cancelCapture(); + expect(thetaClient.cancelCompositeIntervalCapture).toHaveBeenCalled(); + }); + }); + + test('exception', (done) => { + jest.mocked(NativeEventEmitter_addListener).mockImplementation( + jest.fn(() => { + return { + remove: jest.fn(), + }; + }) + ); + const builder = getCompositeIntervalCaptureBuilder(600); + jest + .mocked(thetaClient.buildCompositeIntervalCapture) + .mockImplementation(jest.fn(async () => {})); + jest.mocked(thetaClient.startCompositeIntervalCapture).mockImplementation( + jest.fn(async () => { + throw 'error'; + }) + ); + + builder.build().then((capture) => { + capture + .startCapture() + .then(() => { + expect(true).toBeFalsy(); + }) + .catch((error) => { + expect(error).toBe('error'); + done(); + }); + }); + }); + + test('progress events', 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 = getCompositeIntervalCaptureBuilder(600); + jest + .mocked(thetaClient.buildCompositeIntervalCapture) + .mockImplementation(jest.fn(async () => {})); + const testUrl = 'http://192.168.1.1/files/100RICOH/R100.JPG'; + + const sendProgress = (progress: number) => { + notifyCallback({ + name: 'COMPOSITE-INTERVAL-PROGRESS', + params: { + completion: progress, + }, + }); + }; + + jest.mocked(thetaClient.startCompositeIntervalCapture).mockImplementation( + jest.fn(async () => { + sendProgress(0.5); + return testUrl; + }) + ); + + const capture = await builder.build(); + let isOnProgress = false; + const fileUrl = await capture.startCapture((completion) => { + expect(completion).toBe(0.5); + isOnProgress = true; + }); + expect(fileUrl).toBe(testUrl); + + let done: (value: unknown) => void; + const promise = new Promise((resolve) => { + done = resolve; + }); + + setTimeout(() => { + expect(NotifyController.instance.notifyList.size).toBe(0); + expect(isOnProgress).toBeTruthy(); + done(0); + }, 1); + + return promise; + }); + + test('stop error events', 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 = getCompositeIntervalCaptureBuilder(600); + jest + .mocked(thetaClient.buildCompositeIntervalCapture) + .mockImplementation(jest.fn(async () => {})); + const testUrl = 'http://192.168.1.1/files/100RICOH/R100.JPG'; + + const sendStopError = (message: string) => { + notifyCallback({ + name: 'COMPOSITE-INTERVAL-STOP-ERROR', + params: { + message, + }, + }); + }; + + jest.mocked(thetaClient.startCompositeIntervalCapture).mockImplementation( + jest.fn(async () => { + sendStopError('stop error'); + return testUrl; + }) + ); + + const capture = await builder.build(); + let isOnStopError = false; + const fileUrl = await capture.startCapture( + () => {}, + (error) => { + expect(error.message).toBe('stop error'); + isOnStopError = true; + } + ); + expect(fileUrl).toBe(testUrl); + + let done: (value: unknown) => void; + const promise = new Promise((resolve) => { + done = resolve; + }); + + setTimeout(() => { + expect(NotifyController.instance.notifyList.size).toBe(0); + expect(isOnStopError).toBeTruthy(); + done(0); + }, 1); + + return promise; + }); +}); diff --git a/react-native/src/__tests__/capture/shot-count-specified-interval-capture.test.ts b/react-native/src/__tests__/capture/shot-count-specified-interval-capture.test.ts index 70ff0807de..0283e8b455 100644 --- a/react-native/src/__tests__/capture/shot-count-specified-interval-capture.test.ts +++ b/react-native/src/__tests__/capture/shot-count-specified-interval-capture.test.ts @@ -198,7 +198,7 @@ describe('interval shooting with the shot count specified', () => { jest .mocked(thetaClient.buildShotCountSpecifiedIntervalCapture) .mockImplementation(jest.fn(async () => {})); - const testUrl = 'http://192.168.1.1/files/100RICOH/R100.JPG'; + const testUrls = ['http://192.168.1.1/files/100RICOH/R100.JPG']; const sendProgress = (progress: number) => { notifyCallback({ @@ -214,17 +214,17 @@ describe('interval shooting with the shot count specified', () => { .mockImplementation( jest.fn(async () => { sendProgress(0.5); - return testUrl; + return testUrls; }) ); const capture = await builder.build(); let isOnProgress = false; - const fileUrl = await capture.startCapture((completion) => { + const fileUrls = await capture.startCapture((completion) => { expect(completion).toBe(0.5); isOnProgress = true; }); - expect(fileUrl).toBe(testUrl); + expect(fileUrls).toBe(testUrls); let done: (value: unknown) => void; const promise = new Promise((resolve) => { @@ -258,7 +258,7 @@ describe('interval shooting with the shot count specified', () => { jest .mocked(thetaClient.buildShotCountSpecifiedIntervalCapture) .mockImplementation(jest.fn(async () => {})); - const testUrl = 'http://192.168.1.1/files/100RICOH/R100.JPG'; + const testUrls = ['http://192.168.1.1/files/100RICOH/R100.JPG']; const sendStopError = (message: string) => { notifyCallback({ @@ -274,20 +274,20 @@ describe('interval shooting with the shot count specified', () => { .mockImplementation( jest.fn(async () => { sendStopError('stop error'); - return testUrl; + return testUrls; }) ); const capture = await builder.build(); let isOnStopError = false; - const fileUrl = await capture.startCapture( + const fileUrls = await capture.startCapture( () => {}, (error) => { expect(error.message).toBe('stop error'); isOnStopError = true; } ); - expect(fileUrl).toBe(testUrl); + expect(fileUrls).toBe(testUrls); let done: (value: unknown) => void; const promise = new Promise((resolve) => { diff --git a/react-native/src/__tests__/capture/time-shift-capture.test.ts b/react-native/src/__tests__/capture/time-shift-capture.test.ts index fb8197a81a..b7698461ea 100644 --- a/react-native/src/__tests__/capture/time-shift-capture.test.ts +++ b/react-native/src/__tests__/capture/time-shift-capture.test.ts @@ -235,4 +235,65 @@ describe('time shift capture', () => { return promise; }); + + test('stop error events', 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 = getTimeShiftCaptureBuilder(); + jest + .mocked(thetaClient.buildShotCountSpecifiedIntervalCapture) + .mockImplementation(jest.fn(async () => {})); + const testUrl = 'http://192.168.1.1/files/100RICOH/R100.JPG'; + + const sendStopError = (message: string) => { + notifyCallback({ + name: 'TIME-SHIFT-STOP-ERROR', + params: { + message, + }, + }); + }; + + jest.mocked(thetaClient.startTimeShiftCapture).mockImplementation( + jest.fn(async () => { + sendStopError('stop error'); + return testUrl; + }) + ); + + const capture = await builder.build(); + let isOnStopError = false; + const fileUrl = await capture.startCapture( + () => {}, + (error) => { + expect(error.message).toBe('stop error'); + isOnStopError = true; + } + ); + expect(fileUrl).toBe(testUrl); + + let done: (value: unknown) => void; + const promise = new Promise((resolve) => { + done = resolve; + }); + + setTimeout(() => { + expect(NotifyController.instance.notifyList.size).toBe(0); + expect(isOnStopError).toBeTruthy(); + done(0); + }, 1); + + return promise; + }); }); diff --git a/react-native/src/capture/composite-interval-capture.ts b/react-native/src/capture/composite-interval-capture.ts new file mode 100644 index 0000000000..9838b7ef1b --- /dev/null +++ b/react-native/src/capture/composite-interval-capture.ts @@ -0,0 +1,140 @@ +import { CaptureBuilder } from './capture'; +import { NativeModules } from 'react-native'; +import { + BaseNotify, + NotifyController, +} from '../theta-repository/notify-controller'; +const ThetaClientReactNative = NativeModules.ThetaClientReactNative; + +const NOTIFY_PROGRESS = 'COMPOSITE-INTERVAL-PROGRESS'; +const NOTIFY_STOP_ERROR = 'COMPOSITE-INTERVAL-STOP-ERROR'; + +interface CaptureProgressNotify extends BaseNotify { + params?: { + completion: number; + }; +} + +interface CaptureStopErrorNotify extends BaseNotify { + params?: { + message: string; + }; +} + +/** + * CompositeIntervalCapture class + */ +export class CompositeIntervalCapture { + notify: NotifyController; + constructor(notify: NotifyController) { + this.notify = notify; + } + /** + * start interval composite shooting + * @param onProgress the block for interval composite shooting onProgress + * @return promise of captured file url + */ + async startCapture( + onProgress?: (completion?: number) => void, + onStopFailed?: (error: any) => void + ): Promise { + if (onProgress) { + this.notify.addNotify(NOTIFY_PROGRESS, (event: CaptureProgressNotify) => { + onProgress(event.params?.completion); + }); + } + if (onStopFailed) { + this.notify.addNotify( + NOTIFY_STOP_ERROR, + (event: CaptureStopErrorNotify) => { + onStopFailed(event.params); + } + ); + } + + return new Promise(async (resolve, reject) => { + await ThetaClientReactNative.startCompositeIntervalCapture() + .then((result?: string[]) => { + resolve(result ?? undefined); + }) + .catch((error: any) => { + reject(error); + }) + .finally(() => { + this.notify.removeNotify(NOTIFY_PROGRESS); + this.notify.removeNotify(NOTIFY_STOP_ERROR); + }); + }); + } + + /** + * cancel interval composite shooting + */ + async cancelCapture(): Promise { + return await ThetaClientReactNative.cancelCompositeIntervalCapture(); + } +} + +/** + * CompositeIntervalCaptureBuilder class + */ +export class CompositeIntervalCaptureBuilder extends CaptureBuilder { + interval?: number; + readonly shootingTimeSec: number; + + /** construct CompositeIntervalCaptureBuilder instance */ + constructor(shootingTimeSec: number) { + super(); + + this.interval = undefined; + this.shootingTimeSec = shootingTimeSec; + } + + /** + * set interval of checking interval composite shooting status command + * @param timeMillis interval + * @returns CompositeIntervalCaptureBuilder + */ + setCheckStatusCommandInterval( + timeMillis: number + ): CompositeIntervalCaptureBuilder { + this.interval = timeMillis; + return this; + } + + /** + * Set In-progress save interval for interval composite shooting (sec). + * @param {interval} sec sec. + * @return CompositeIntervalCaptureBuilder + */ + setCompositeShootingOutputInterval( + sec: number + ): CompositeIntervalCaptureBuilder { + this.options.compositeShootingOutputInterval = sec; + return this; + } + + /** + * Builds an instance of a CompositeIntervalCapture that has all the combined + * parameters of the Options that have been added to the Builder. + * @return promise of CompositeIntervalCapture instance + */ + build(): Promise { + let params = { + ...this.options, + // Cannot pass negative values in IOS, use objects + _capture_interval: this.interval ?? -1, + }; + return new Promise(async (resolve, reject) => { + try { + await ThetaClientReactNative.getCompositeIntervalCaptureBuilder( + this.shootingTimeSec + ); + await ThetaClientReactNative.buildCompositeIntervalCapture(params); + resolve(new CompositeIntervalCapture(NotifyController.instance)); + } catch (error) { + reject(error); + } + }); + } +} diff --git a/react-native/src/capture/index.ts b/react-native/src/capture/index.ts index 0a59aae274..a5f88412ea 100644 --- a/react-native/src/capture/index.ts +++ b/react-native/src/capture/index.ts @@ -4,3 +4,4 @@ export * from './video-capture'; export * from './time-shift-capture'; export * from './limitless-interval-capture'; export * from './shot-count-specified-interval-capture'; +export * from './composite-interval-capture'; diff --git a/react-native/src/capture/shot-count-specified-interval-capture.ts b/react-native/src/capture/shot-count-specified-interval-capture.ts index f9f8dc0db2..5ea209d7c5 100644 --- a/react-native/src/capture/shot-count-specified-interval-capture.ts +++ b/react-native/src/capture/shot-count-specified-interval-capture.ts @@ -32,6 +32,7 @@ export class ShotCountSpecifiedIntervalCapture { /** * start interval shooting with the shot count specified * @param onProgress the block for interval shooting with the shot count specified onProgress + * @param onStopFailed the block for error of cancelCapture * @return promise of captured file url */ async startCapture( diff --git a/react-native/src/capture/time-shift-capture.ts b/react-native/src/capture/time-shift-capture.ts index 33febef0e4..7db1472890 100644 --- a/react-native/src/capture/time-shift-capture.ts +++ b/react-native/src/capture/time-shift-capture.ts @@ -8,6 +8,7 @@ import { const ThetaClientReactNative = NativeModules.ThetaClientReactNative; const NOTIFY_NAME = 'TIME-SHIFT-PROGRESS'; +const NOTIFY_STOP_ERROR = 'TIME-SHIFT-STOP-ERROR'; interface CaptureProgressNotify extends BaseNotify { params?: { @@ -15,6 +16,12 @@ interface CaptureProgressNotify extends BaseNotify { }; } +interface CaptureStopErrorNotify extends BaseNotify { + params?: { + message: string; + }; +} + /** * TimeShiftCapture class */ @@ -26,16 +33,26 @@ export class TimeShiftCapture { /** * start time-shift * @param onProgress the block for time-shift onProgress + * @param onStopFailed the block for error of cancelCapture * @return promise of captured file url */ async startCapture( - onProgress?: (completion?: number) => void + onProgress?: (completion?: number) => void, + onStopFailed?: (error: any) => void ): Promise { if (onProgress) { this.notify.addNotify(NOTIFY_NAME, (event: CaptureProgressNotify) => { onProgress(event.params?.completion); }); } + if (onStopFailed) { + this.notify.addNotify( + NOTIFY_STOP_ERROR, + (event: CaptureStopErrorNotify) => { + onStopFailed(event.params); + } + ); + } return new Promise(async (resolve, reject) => { await ThetaClientReactNative.startTimeShiftCapture() @@ -47,6 +64,7 @@ export class TimeShiftCapture { }) .finally(() => { this.notify.removeNotify(NOTIFY_NAME); + this.notify.removeNotify(NOTIFY_STOP_ERROR); }); }); } diff --git a/react-native/src/theta-repository/options/index.ts b/react-native/src/theta-repository/options/index.ts index e0335a572e..5e0cb0973f 100644 --- a/react-native/src/theta-repository/options/index.ts +++ b/react-native/src/theta-repository/options/index.ts @@ -1,5 +1,6 @@ export * from './options'; export * from './option-ai-auto-thumbnail'; +export * from './option-auto-bracket'; export * from './option-bitrate'; export * from './option-burst-mode'; export * from './option-burst-option'; diff --git a/react-native/src/theta-repository/options/option-auto-bracket.ts b/react-native/src/theta-repository/options/option-auto-bracket.ts new file mode 100644 index 0000000000..14176161e2 --- /dev/null +++ b/react-native/src/theta-repository/options/option-auto-bracket.ts @@ -0,0 +1,48 @@ +import type { + ApertureEnum, + ExposureCompensationEnum, + ExposureProgramEnum, + IsoEnum, + WhiteBalanceEnum, +} from './options'; +import type { ShutterSpeedEnum } from './option-shutter-speed'; + +/** + * Multi bracket shooting setting + */ +export type BracketSetting = { + /** + * @see ApertureEnum + */ + aperture?: ApertureEnum; + + /** + * Color temperature of the camera (Kelvin), 2,500 to 10,000 in 100-Kelvin units. + */ + colorTemperature?: number; + + /** + * @see ExposureCompensationEnum + */ + exposureCompensation?: ExposureCompensationEnum; + + /** + * @see ExposureProgramEnum + */ + exposureProgram?: ExposureProgramEnum; + + /** + * @see IsoEnum + */ + iso?: IsoEnum; + + /** + * @see ShutterSpeedEnum + */ + shutterSpeed?: ShutterSpeedEnum; + + /** + * @see WhiteBalanceEnum + */ + whiteBalance?: WhiteBalanceEnum; +}; diff --git a/react-native/src/theta-repository/options/options.ts b/react-native/src/theta-repository/options/options.ts index d7bd33bf70..2d689da04e 100644 --- a/react-native/src/theta-repository/options/options.ts +++ b/react-native/src/theta-repository/options/options.ts @@ -1,5 +1,6 @@ import type { AiAutoThumbnailEnum } from './option-ai-auto-thumbnail'; import type { BitrateEnum } from './option-bitrate'; +import type { BracketSetting } from './option-auto-bracket'; import type { BurstModeEnum } from './option-burst-mode'; import type { BurstOption } from './option-burst-option'; import type { CameraControlSourceEnum } from './option-camera-control-source'; @@ -451,6 +452,8 @@ export const OptionNameEnum = { AiAutoThumbnail: 'AiAutoThumbnail', /** aperture */ Aperture: 'Aperture', + /** autoBracket */ + AutoBracket: 'AutoBracket', /** _bitrate*/ Bitrate: 'Bitrate', /** _bluetoothPower*/ @@ -571,6 +574,8 @@ export type Options = { aiAutoThumbnail?: AiAutoThumbnailEnum; /** Aperture value. */ aperture?: ApertureEnum; + /** */ + autoBracket?: BracketSetting[]; /** Bitrate */ bitrate?: BitrateEnum; /** BluetoothPower */ diff --git a/react-native/src/theta-repository/theta-repository.ts b/react-native/src/theta-repository/theta-repository.ts index 5f7b0ce192..720b3a43e3 100644 --- a/react-native/src/theta-repository/theta-repository.ts +++ b/react-native/src/theta-repository/theta-repository.ts @@ -18,6 +18,7 @@ import { TimeShiftCaptureBuilder, LimitlessIntervalCaptureBuilder, ShotCountSpecifiedIntervalCaptureBuilder, + CompositeIntervalCaptureBuilder, } from '../capture'; import type { ThetaConfig } from './theta-config'; import type { ThetaTimeout } from './theta-timeout'; @@ -117,6 +118,19 @@ export function getShotCountSpecifiedIntervalCaptureBuilder( return new ShotCountSpecifiedIntervalCaptureBuilder(shotCount); } +/** + * Get CompositeIntervalCapture.Builder for take interval composite shooting. + * + * @function getCompositeIntervalCaptureBuilder + * @param {shootingTimeSec} Shooting time for interval composite shooting (sec) + * @return created CompositeIntervalCaptureBuilder instance + */ +export function getCompositeIntervalCaptureBuilder( + shootingTimeSec: number +): CompositeIntervalCaptureBuilder { + return new CompositeIntervalCaptureBuilder(shootingTimeSec); +} + /** * Start live preview. * preview frame (JPEG DataURL) send THETA_EVENT_NAME event as diff --git a/react-native/verification-tool/android/app/build.gradle b/react-native/verification-tool/android/app/build.gradle index 6ad665f041..eede5f8e34 100644 --- a/react-native/verification-tool/android/app/build.gradle +++ b/react-native/verification-tool/android/app/build.gradle @@ -107,6 +107,7 @@ android { reset() enable enableSeparateBuildPerCPUArchitecture universalApk false // If true, also generate a universal APK + //noinspection ChromeOsAbiSupport include (*reactNativeArchitectures()) } } @@ -158,7 +159,7 @@ dependencies { // The version of react-native is set by the React Native Gradle Plugin implementation("com.facebook.react:react-android") - implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.0.0") + implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") { diff --git a/react-native/verification-tool/android/build.gradle b/react-native/verification-tool/android/build.gradle index fb37a26db3..a052002150 100644 --- a/react-native/verification-tool/android/build.gradle +++ b/react-native/verification-tool/android/build.gradle @@ -6,6 +6,7 @@ buildscript { minSdkVersion = 26 compileSdkVersion = 33 targetSdkVersion = 33 + ext.kotlinVersion = "1.9.10" // We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP. ndkVersion = "23.1.7779620" diff --git a/react-native/verification-tool/package.json b/react-native/verification-tool/package.json index 599e4e9edc..6eebb0ffc4 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.7", + "react-native": "0.71.14", "react-native-safe-area-context": "^4.7.1", "react-native-screens": "^3.24.0", "react-native-webview": "^13.6.0" @@ -23,6 +23,7 @@ "@babel/core": "^7.20.0", "@babel/preset-env": "^7.20.0", "@babel/runtime": "^7.20.0", + "@types/lodash": "^4.14.200", "babel-plugin-module-resolver": "^4.1.0", "metro-react-native-babel-preset": "0.73.9" } diff --git a/react-native/verification-tool/src/App.tsx b/react-native/verification-tool/src/App.tsx index 533b51979a..e30275507a 100644 --- a/react-native/verification-tool/src/App.tsx +++ b/react-native/verification-tool/src/App.tsx @@ -19,6 +19,7 @@ import GetInfoScreen from './screen/get-info-screen/get-info-screen'; import TimeShiftCaptureScreen from './screen/time-shift-capture-screen/time-shift-capture-screen'; import LimitlessIntervalCaptureScreen from './screen/limitless-interval-capture-screen/limitless-interval-capture-screen'; import ShotCountSpecifiedIntervalCaptureScreen from './screen/shot-count-specified-interval-capture-screen/shot-count-specified-interval-capture-screen'; +import CompositeIntervalCaptureScreen from './screen/composite-interval-capture-screen/composite-interval-capture-screen'; const Stack = createNativeStackNavigator(); @@ -107,6 +108,11 @@ const App = () => { name="shotCountSpecifiedIntervalCapture" component={ShotCountSpecifiedIntervalCaptureScreen} /> + void; +}) => { + return ( + + { + onChange({ ...item, aperture }); + }} + bracketSetting={item} + enumValues={ApertureEnum} + /> + { + onChange({ ...item, colorTemperature: value }); + }} + value={item.colorTemperature} + /> + { + onChange({ ...item, exposureCompensation }); + }} + bracketSetting={item} + enumValues={ExposureCompensationEnum} + /> + { + onChange({ ...item, exposureProgram }); + }} + bracketSetting={item} + enumValues={ExposureProgramEnum} + /> + { + onChange({ ...item, iso }); + }} + bracketSetting={item} + enumValues={IsoEnum} + /> + { + onChange({ ...item, shutterSpeed }); + }} + bracketSetting={item} + enumValues={ShutterSpeedEnum} + /> + { + onChange({ ...item, whiteBalance }); + }} + bracketSetting={item} + enumValues={WhiteBalanceEnum} + /> + + ); +}; + +interface BracketSettingMenuProps { + item: BracketSetting; + index: number; + onChange: (index: number, setting: BracketSetting) => void; + onRemove: (index: number) => void; +} + +const BracketSettingMenu: React.FC = ({ + item, + index, + onChange, + onRemove, +}) => { + return ( + + + {'Bracket parameters: ' + (index + 1)} + + +