From b0a86c105aa1527e9e1be3d177d9af902276104e Mon Sep 17 00:00:00 2001 From: osakila Date: Thu, 21 Nov 2024 09:27:39 +0900 Subject: [PATCH 1/3] Add progress to convertVideoFormats --- .../ThetaClientFlutterPlugin.kt | 12 +- .../SwiftThetaClientFlutterPlugin.swift | 11 +- flutter/lib/theta_client_flutter.dart | 9 +- .../theta_client_flutter_method_channel.dart | 22 ++- ...eta_client_flutter_platform_interface.dart | 4 +- ...ert_video_formats_method_channel_test.dart | 122 +++++++++++++ ...ta_client_flutter_method_channel_test.dart | 21 --- flutter/test/theta_client_flutter_test.dart | 6 +- .../ricoh360/thetaclient/ThetaRepository.kt | 13 +- .../repository/ConvertVideoFormatsTest.kt | 16 +- .../ThetaClientSdkModule.kt | 11 +- react-native/ios/ThetaClientReactNative.swift | 11 +- react-native/src/__mocks__/react-native.ts | 1 + .../convert-video-formats.test.ts | 161 ++++++++++++++++++ .../libs/convert-video-formats.ts | 41 +++++ .../src/theta-repository/libs/index.ts | 1 + .../src/theta-repository/notify-controller.ts | 4 + .../src/theta-repository/theta-repository.ts | 10 +- .../video-convert-screen.tsx | 5 +- 19 files changed, 439 insertions(+), 42 deletions(-) create mode 100644 flutter/test/commands/convert_video_formats_method_channel_test.dart create mode 100644 react-native/src/__tests__/theta-repository/convert-video-formats.test.ts create mode 100644 react-native/src/theta-repository/libs/convert-video-formats.ts create mode 100644 react-native/src/theta-repository/libs/index.ts 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 eb44ddd50b..30268069e7 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 @@ -87,6 +87,7 @@ class ThetaClientFlutterPlugin : FlutterPlugin, MethodCallHandler { const val notifyIdVideoCaptureStopError = 10081 const val notifyIdVideoCaptureCapturing = 10082 const val notifyIdVideoCaptureStarted = 10083 + const val notifyIdConvertVideoFormatsProgress = 10091 } fun sendNotifyEvent(id: Int, params: Map) { @@ -1486,7 +1487,16 @@ class ThetaClientFlutterPlugin : FlutterPlugin, MethodCallHandler { val fileUrl = call.argument("fileUrl")!! val toLowResolution = call.argument("toLowResolution")!! val applyTopBottomCorrection = call.argument("applyTopBottomCorrection")!! - val response = thetaRepository!!.convertVideoFormats(fileUrl, toLowResolution, applyTopBottomCorrection) + val response = thetaRepository!!.convertVideoFormats( + fileUrl, + toLowResolution, + applyTopBottomCorrection + ) { completion -> + sendNotifyEvent( + notifyIdConvertVideoFormatsProgress, + toCaptureProgressNotifyParam(completion) + ) + } result.success(response) } catch (e: Exception) { result.error(e.javaClass.simpleName, e.message, null) diff --git a/flutter/ios/Classes/SwiftThetaClientFlutterPlugin.swift b/flutter/ios/Classes/SwiftThetaClientFlutterPlugin.swift index 0a310a300a..8d175b61fb 100644 --- a/flutter/ios/Classes/SwiftThetaClientFlutterPlugin.swift +++ b/flutter/ios/Classes/SwiftThetaClientFlutterPlugin.swift @@ -27,6 +27,7 @@ let NOTIFY_PHOTO_CAPTURING = 10071 let NOTIFY_VIDEO_CAPTURE_STOP_ERROR = 10081 let NOTIFY_VIDEO_CAPTURE_CAPTURING = 10082 let NOTIFY_VIDEO_CAPTURE_STARTED = 10083 +let NOTIFY_CONVERT_VIDEO_FORMATS_PROGRESS = 10091 public class SwiftThetaClientFlutterPlugin: NSObject, FlutterPlugin, FlutterStreamHandler { public func onListen(withArguments _: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { @@ -1422,7 +1423,7 @@ public class SwiftThetaClientFlutterPlugin: NSObject, FlutterPlugin, FlutterStre } func convertVideoFormats(call: FlutterMethodCall, result: @escaping FlutterResult) { - if thetaRepository == nil { + guard let thetaRepository = thetaRepository else { let flutterError = FlutterError(code: SwiftThetaClientFlutterPlugin.errorCode, message: SwiftThetaClientFlutterPlugin.messageNotInit, details: nil) result(flutterError) return @@ -1431,7 +1432,13 @@ public class SwiftThetaClientFlutterPlugin: NSObject, FlutterPlugin, FlutterStre let fileUrl = arguments["fileUrl"] as! String let toLowResolution = arguments["toLowResolution"] as! Bool let applyTopBottomCorrection = arguments["applyTopBottomCorrection"] as! Bool - thetaRepository!.convertVideoFormats(fileUrl: fileUrl, toLowResolution: toLowResolution, applyTopBottomCorrection: applyTopBottomCorrection) { response, error in + thetaRepository.convertVideoFormats( + fileUrl: fileUrl, + toLowResolution: toLowResolution, + applyTopBottomCorrection: applyTopBottomCorrection + ) { completion in + self.sendNotifyEvent(id: NOTIFY_CONVERT_VIDEO_FORMATS_PROGRESS, params: toCaptureProgressNotifyParam(value: completion.floatValue)) + } completionHandler: { response, error in if let thetaError = error { let flutterError = FlutterError(code: SwiftThetaClientFlutterPlugin.errorCode, message: thetaError.localizedDescription, details: nil) result(flutterError) diff --git a/flutter/lib/theta_client_flutter.dart b/flutter/lib/theta_client_flutter.dart index 6b15b172b5..b386bd6db6 100644 --- a/flutter/lib/theta_client_flutter.dart +++ b/flutter/lib/theta_client_flutter.dart @@ -260,12 +260,15 @@ class ThetaClientFlutter { /// - @param fileUrl URL of a saved movie file. /// - @param toLowResolution If true generates lower resolution video, otherwise same resolution. /// - @param applyTopBottomCorrection apply Top/bottom correction. This parameter is ignored on Theta X. + /// - @param onProgress the block for convertVideoFormats progress. /// - @return URL of a converted movie file. /// - @throws Command is currently disabled. - Future convertVideoFormats(String fileUrl, bool toLowResolution, - [bool applyTopBottomCorrection = true]) { + Future convertVideoFormats(String fileUrl, + bool toLowResolution, + [bool applyTopBottomCorrection = true, + void Function(double)? onProgress]) { return ThetaClientFlutterPlatform.instance.convertVideoFormats( - fileUrl, toLowResolution, applyTopBottomCorrection); + fileUrl, toLowResolution, applyTopBottomCorrection, onProgress); } /// Cancels the movie format conversion. diff --git a/flutter/lib/theta_client_flutter_method_channel.dart b/flutter/lib/theta_client_flutter_method_channel.dart index 357f7223bb..0cf9e6440a 100644 --- a/flutter/lib/theta_client_flutter_method_channel.dart +++ b/flutter/lib/theta_client_flutter_method_channel.dart @@ -31,6 +31,7 @@ const notifyIdPhotoCapturing = 10071; const notifyIdVideoCaptureStopError = 10081; const notifyIdVideoCaptureCapturing = 10082; const notifyIdVideoCaptureStarted = 10083; +const notifyIdConvertVideoFormatsProgress = 10091; /// An implementation of [ThetaClientFlutterPlatform] that uses method channels. class MethodChannelThetaClientFlutter extends ThetaClientFlutterPlatform { @@ -45,6 +46,10 @@ class MethodChannelThetaClientFlutter extends ThetaClientFlutterPlatform { notifyList[id] = callback; } + bool existsNotify(int id) { + return notifyList.containsKey(id); + } + void removeNotify(int id) { notifyList.remove(id); } @@ -891,9 +896,22 @@ class MethodChannelThetaClientFlutter extends ThetaClientFlutterPlatform { @override Future convertVideoFormats(String fileUrl, bool toLowResolution, - bool applyTopBottomCorrection) async { + bool applyTopBottomCorrection, void Function(double)? onProgress) async { var completer = Completer(); + if (existsNotify(notifyIdConvertVideoFormatsProgress)) { + completer.completeError(Exception('convertVideoFormats is running.')); + return completer.future; + } + try { + enableNotifyEventReceiver(); + addNotify(notifyIdConvertVideoFormatsProgress, (params) { + final completion = params?['completion'] as double?; + if (onProgress != null && completion != null) { + onProgress(completion); + } + }); + final Map params = { 'fileUrl': fileUrl, 'toLowResolution': toLowResolution, @@ -904,6 +922,8 @@ class MethodChannelThetaClientFlutter extends ThetaClientFlutterPlatform { completer.complete(result); } catch (e) { completer.completeError(e); + } finally { + removeNotify(notifyIdConvertVideoFormatsProgress); } return completer.future; } diff --git a/flutter/lib/theta_client_flutter_platform_interface.dart b/flutter/lib/theta_client_flutter_platform_interface.dart index 38bc015f21..89418165f0 100644 --- a/flutter/lib/theta_client_flutter_platform_interface.dart +++ b/flutter/lib/theta_client_flutter_platform_interface.dart @@ -303,8 +303,8 @@ abstract class ThetaClientFlutterPlatform extends PlatformInterface { throw UnimplementedError('stopSelfTimer() has not been implemented.'); } - Future convertVideoFormats( - String fileUrl, bool toLowResolution, bool applyTopBottomCorrection) { + Future convertVideoFormats(String fileUrl, bool toLowResolution, + bool applyTopBottomCorrection, void Function(double)? onProgress) { throw UnimplementedError('convertVideoFormats() has not been implemented.'); } diff --git a/flutter/test/commands/convert_video_formats_method_channel_test.dart b/flutter/test/commands/convert_video_formats_method_channel_test.dart new file mode 100644 index 0000000000..fefdd5a855 --- /dev/null +++ b/flutter/test/commands/convert_video_formats_method_channel_test.dart @@ -0,0 +1,122 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.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(); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, null); + }); + + tearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, null); + }); + + test('convertVideoFormats', () async { + String fileUrl = 'http://dummy.MP4'; + String result = 'http://dummy_result.MP4'; + bool toLowResolution = true; + bool applyTopBottomCorrection = true; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { + expect(methodCall.method, 'convertVideoFormats'); + expect(platform.notifyList.containsKey(10091), true, + reason: 'add notify progress'); + + var arguments = methodCall.arguments as Map; + expect(arguments['fileUrl'], fileUrl); + expect(arguments['toLowResolution'], toLowResolution); + expect(arguments['applyTopBottomCorrection'], applyTopBottomCorrection); + + // native event + platform.onNotify({ + 'id': 10091, + 'params': { + 'completion': 0.1, + }, + }); + await Future.delayed(const Duration(milliseconds: 10)); + platform.onNotify({ + 'id': 10091, + 'params': { + 'completion': 0.2, + }, + }); + await Future.delayed(const Duration(milliseconds: 10)); + + return Future.value(result); + }); + + int progressCount = 0; + expect( + await platform.convertVideoFormats( + fileUrl, toLowResolution, applyTopBottomCorrection, (completion) { + progressCount += 1; + }), + result); + expect(progressCount, 2); + expect(platform.notifyList.length, 0); + + expect( + await platform.convertVideoFormats( + fileUrl, toLowResolution, applyTopBottomCorrection, null), + result); + }); + + test('error call in running', () async { + String fileUrl = 'http://dummy.MP4'; + String result = 'http://dummy_result.MP4'; + bool toLowResolution = true; + bool applyTopBottomCorrection = true; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { + expect(methodCall.method, 'convertVideoFormats'); + await Future.delayed(const Duration(milliseconds: 10)); + + return Future.value(result); + }); + + var future = platform.convertVideoFormats( + fileUrl, toLowResolution, applyTopBottomCorrection, (completion) {}); + + try { + await platform.convertVideoFormats( + fileUrl, toLowResolution, applyTopBottomCorrection, (completion) {}); + expect(true, false, reason: 'not exception'); + } catch (error) { + expect( + error.toString().contains('convertVideoFormats is running.'), true); + } + await future.then((value) { + expect(value, result); + }); + expect(platform.notifyList.length, 0); + }); + + test('exception', () async { + String fileUrl = 'http://dummy.MP4'; + bool toLowResolution = true; + bool applyTopBottomCorrection = true; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { + expect(methodCall.method, 'convertVideoFormats'); + throw Exception('test error'); + }); + + try { + await platform.convertVideoFormats( + fileUrl, toLowResolution, applyTopBottomCorrection, (completion) {}); + expect(true, false, reason: 'not exception'); + } catch (error) { + expect(error.toString().contains('test error'), true); + } + expect(platform.notifyList.length, 0); + }); +} diff --git a/flutter/test/theta_client_flutter_method_channel_test.dart b/flutter/test/theta_client_flutter_method_channel_test.dart index ed9351a58f..8e8c1f239a 100644 --- a/flutter/test/theta_client_flutter_method_channel_test.dart +++ b/flutter/test/theta_client_flutter_method_channel_test.dart @@ -1243,27 +1243,6 @@ void main() { await platform.stopSelfTimer(); }); - test('convertVideoFormats', () async { - String fileUrl = 'http://dummy.MP4'; - String result = 'http://dummy_result.MP4'; - bool toLowResolution = true; - bool applyTopBottomCorrection = true; - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(channel, (MethodCall methodCall) async { - expect(methodCall.method, 'convertVideoFormats'); - - var arguments = methodCall.arguments as Map; - expect(arguments['fileUrl'], fileUrl); - expect(arguments['toLowResolution'], toLowResolution); - expect(arguments['applyTopBottomCorrection'], applyTopBottomCorrection); - return Future.value(result); - }); - expect( - await platform.convertVideoFormats( - fileUrl, toLowResolution, applyTopBottomCorrection), - result); - }); - test('cancelVideoConvert', () async { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(channel, (MethodCall methodCall) async { diff --git a/flutter/test/theta_client_flutter_test.dart b/flutter/test/theta_client_flutter_test.dart index b3a30e6247..86e6148b0c 100644 --- a/flutter/test/theta_client_flutter_test.dart +++ b/flutter/test/theta_client_flutter_test.dart @@ -313,8 +313,10 @@ class MockThetaClientFlutterPlatform } @override - Future convertVideoFormats( - String fileUrl, bool toLowResolution, bool applyTopBottomCorrection) { + Future convertVideoFormats(String fileUrl, + bool toLowResolution, + bool applyTopBottomCorrection, + void Function(double)? onProgress) { return Future.value(''); } 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 c27d523db8..c4ed78c69b 100644 --- a/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/ThetaRepository.kt +++ b/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/ThetaRepository.kt @@ -7140,12 +7140,18 @@ class ThetaRepository internal constructor(val endpoint: String, config: Config? * @param fileUrl URL of a saved movie file. * @param toLowResolution If true generates lower resolution video, otherwise same resolution. * @param applyTopBottomCorrection apply Top/bottom correction. This parameter is ignored on Theta X. + * @param progress the block for convertVideoFormats progress. * @return URL of a converted movie file. * @exception ThetaWebApiException Command is currently disabled. * @exception NotConnectedException */ @Throws(Throwable::class) - suspend fun convertVideoFormats(fileUrl: String, toLowResolution: Boolean, applyTopBottomCorrection: Boolean = true): String { + suspend fun convertVideoFormats( + fileUrl: String, + toLowResolution: Boolean, + applyTopBottomCorrection: Boolean = true, + progress: ((completion: Float) -> Unit)? = null + ): String { val params = when { cameraModel == ThetaModel.THETA_X -> { if (!toLowResolution) { @@ -7176,6 +7182,11 @@ class ThetaRepository internal constructor(val endpoint: String, config: Config? convertVideoFormatsResponse = ThetaApi.callConvertVideoFormatsCommand(endpoint, params) val id = convertVideoFormatsResponse.id while (convertVideoFormatsResponse.state == CommandState.IN_PROGRESS) { + progress?.apply { + convertVideoFormatsResponse.progress?.completion?.let { + progress(it) + } + } delay(CHECK_COMMAND_STATUS_INTERVAL) convertVideoFormatsResponse = ThetaApi.callStatusApi( endpoint, diff --git a/kotlin-multiplatform/src/commonTest/kotlin/com/ricoh360/thetaclient/repository/ConvertVideoFormatsTest.kt b/kotlin-multiplatform/src/commonTest/kotlin/com/ricoh360/thetaclient/repository/ConvertVideoFormatsTest.kt index 6fc401c8c4..d157d6b6b5 100644 --- a/kotlin-multiplatform/src/commonTest/kotlin/com/ricoh360/thetaclient/repository/ConvertVideoFormatsTest.kt +++ b/kotlin-multiplatform/src/commonTest/kotlin/com/ricoh360/thetaclient/repository/ConvertVideoFormatsTest.kt @@ -12,7 +12,6 @@ import io.ktor.client.request.HttpRequestData import io.ktor.http.HttpStatusCode import io.ktor.http.content.TextContent import io.ktor.utils.io.ByteReadChannel -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json @@ -20,9 +19,10 @@ import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertTrue -@OptIn(ExperimentalSerializationApi::class, ExperimentalCoroutinesApi::class) +@OptIn(ExperimentalSerializationApi::class) class ConvertVideoFormatsTest { private val endpoint = "http://192.168.1.1:80/" @@ -105,12 +105,22 @@ class ConvertVideoFormatsTest { val thetaRepository = ThetaRepository(endpoint) thetaRepository.cameraModel = cameraModel - val response = thetaRepository.convertVideoFormats(fileUrl, toLowResolution, applyTopBottomCorrection) + var isProgress = false + val response = thetaRepository.convertVideoFormats( + fileUrl, + toLowResolution, + applyTopBottomCorrection + ) { completion -> + assertEquals(completion, 0.98f, "completion") + isProgress = true + } if (noConvert) { assertEquals(response, fileUrl, "No convert") + assertFalse(isProgress) } else { assertTrue(response != fileUrl, "convert file url") assertTrue(response.startsWith("http://"), "convert file url") + assertTrue(isProgress) } } 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 10d7e2de26..d797bd3fc5 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 @@ -1632,7 +1632,15 @@ class ThetaClientReactNativeModule( } launch { try { - val convertedUrl = theta.convertVideoFormats(fileUrl, is4k, applyTopBottomCorrection) + val convertedUrl = + theta.convertVideoFormats(fileUrl, is4k, applyTopBottomCorrection) { completion -> + sendNotifyEvent( + toNotify( + NOTIFY_CONVERT_VIDEO_FORMATS_PROGRESS, + toCaptureProgressNotifyParam(value = completion) + ) + ) + } promise.resolve(convertedUrl) } catch (t: Throwable) { promise.reject(t) @@ -2208,5 +2216,6 @@ class ThetaClientReactNativeModule( const val NOTIFY_CONTINUOUS_CAPTURING = "CONTINUOUS-CAPTURING" const val NOTIFY_EVENT_WEBSOCKET_EVENT = "EVENT-WEBSOCKET-EVENT" const val NOTIFY_EVENT_WEBSOCKET_CLOSE = "EVENT-WEBSOCKET-CLOSE" + const val NOTIFY_CONVERT_VIDEO_FORMATS_PROGRESS = "CONVERT-VIDEO-FORMATS-PROGRESS" } } diff --git a/react-native/ios/ThetaClientReactNative.swift b/react-native/ios/ThetaClientReactNative.swift index afc6c0010a..5e967ffe26 100644 --- a/react-native/ios/ThetaClientReactNative.swift +++ b/react-native/ios/ThetaClientReactNative.swift @@ -92,6 +92,7 @@ class ThetaClientReactNative: RCTEventEmitter { static let NOTIFY_CONTINUOUS_CAPTURING = "CONTINUOUS-CAPTURING" static let NOTIFY_EVENT_WEBSOCKET_EVENT = "EVENT-WEBSOCKET-EVENT" static let NOTIFY_EVENT_WEBSOCKET_CLOSE = "EVENT-WEBSOCKET-CLOSE" + static let NOTIFY_CONVERT_VIDEO_FORMATS_PROGRESS = "CONVERT-VIDEO-FORMATS-PROGRESS" @objc override func supportedEvents() -> [String]! { @@ -1837,7 +1838,15 @@ class ThetaClientReactNative: RCTEventEmitter { fileUrl: fileUrl, toLowResolution: toLowResolution, applyTopBottomCorrection: applyTopBottomCorrection - ) { fileUrl, error in + ) { completion in + self.sendEvent( + withName: ThetaClientReactNative.EVENT_NOTIFY, + body: toNotify( + name: ThetaClientReactNative.NOTIFY_CONVERT_VIDEO_FORMATS_PROGRESS, + params: toCaptureProgressNotifyParam(value: completion.floatValue) + ) + ) + } completionHandler: { fileUrl, error in if let error { reject(ERROR_CODE_ERROR, error.localizedDescription, error) } else if let fileUrl { diff --git a/react-native/src/__mocks__/react-native.ts b/react-native/src/__mocks__/react-native.ts index 32077988cc..453ed9b333 100644 --- a/react-native/src/__mocks__/react-native.ts +++ b/react-native/src/__mocks__/react-native.ts @@ -41,6 +41,7 @@ export const NativeModules = { getEventWebSocket: jest.fn(), eventWebSocketStart: jest.fn(), eventWebSocketStop: jest.fn(), + convertVideoFormats: jest.fn(), }, }; diff --git a/react-native/src/__tests__/theta-repository/convert-video-formats.test.ts b/react-native/src/__tests__/theta-repository/convert-video-formats.test.ts new file mode 100644 index 0000000000..1183d236d1 --- /dev/null +++ b/react-native/src/__tests__/theta-repository/convert-video-formats.test.ts @@ -0,0 +1,161 @@ +import { NativeModules } from 'react-native'; +import { convertVideoFormats, initialize } from '../../theta-repository'; +import { NativeEventEmitter_addListener } from '../../__mocks__/react-native'; +import { + BaseNotify, + NotifyController, +} from '../../theta-repository/notify-controller'; + +describe('convertVideoFormats', () => { + const thetaClient = NativeModules.ThetaClientReactNative; + + beforeEach(() => { + thetaClient.convertVideoFormats = jest.fn(); + NotifyController.instance.release(); + jest.clearAllMocks(); + }); + + afterEach(() => { + NotifyController.instance.release(); + jest.clearAllMocks(); + }); + + test('convertVideoFormats', async () => { + const inUrl = 'http://192.168.1.1/files/100RICOH/R0013330.MP4'; + const outUrl = + 'http://192.168.1.1/files/100RICOH/R0013330_er_2K_H264_tbc.MP4'; + const toLowResolution = true; + const applyTopBottomCorrection = true; + jest.mocked(thetaClient.convertVideoFormats).mockImplementation( + jest.fn(async () => { + return outUrl; + }) + ); + + const result = await convertVideoFormats( + inUrl, + toLowResolution, + applyTopBottomCorrection + ); + expect(result).toBe(outUrl); + expect(thetaClient.convertVideoFormats).toHaveBeenCalledTimes(1); + expect(thetaClient.convertVideoFormats).toHaveBeenCalledWith( + inUrl, + toLowResolution, + applyTopBottomCorrection + ); + }); + + test('onProgress', async () => { + let notifyCallback: (notify: BaseNotify) => void = () => { + expect(true).toBeFalsy(); + }; + jest.mocked(NativeEventEmitter_addListener).mockImplementation( + jest.fn((_, callback) => { + notifyCallback = callback; + return { + remove: jest.fn(), + }; + }) + ); + const sendProgress = (completion: number) => { + notifyCallback({ + name: 'CONVERT-VIDEO-FORMATS-PROGRESS', + params: { + completion, + }, + }); + }; + + const inUrl = 'http://192.168.1.1/files/100RICOH/R0013330.MP4'; + const outUrl = + 'http://192.168.1.1/files/100RICOH/R0013330_er_2K_H264_tbc.MP4'; + const toLowResolution = true; + const applyTopBottomCorrection = true; + jest.mocked(thetaClient.convertVideoFormats).mockImplementation( + jest.fn(async () => { + sendProgress(0.8); + return outUrl; + }) + ); + + await initialize(); + let isOnProgress = false; + const result = await convertVideoFormats( + inUrl, + toLowResolution, + applyTopBottomCorrection, + (completion) => { + expect(completion).toBe(0.8); + isOnProgress = true; + } + ); + + expect(result).toBe(outUrl); + expect(isOnProgress).toBeTruthy(); + expect(thetaClient.convertVideoFormats).toHaveBeenCalledTimes(1); + expect(thetaClient.convertVideoFormats).toHaveBeenCalledWith( + inUrl, + toLowResolution, + applyTopBottomCorrection + ); + expect(NotifyController.instance.notifyList.size).toBe(0); + }); + + test('error of running', async () => { + const inUrl = 'http://192.168.1.1/files/100RICOH/R0013330.MP4'; + const outUrl = + 'http://192.168.1.1/files/100RICOH/R0013330_er_2K_H264_tbc.MP4'; + const toLowResolution = true; + const applyTopBottomCorrection = true; + jest.mocked(thetaClient.convertVideoFormats).mockImplementation( + jest.fn(async () => { + return outUrl; + }) + ); + + let isOnError = false; + convertVideoFormats(inUrl, toLowResolution, applyTopBottomCorrection).then( + (url) => { + expect(url).toBe(outUrl); + } + ); + try { + await convertVideoFormats( + inUrl, + toLowResolution, + applyTopBottomCorrection + ); + } catch (error: any) { + isOnError = true; + console.log(`${error}`); + expect(error.message).toBe('convertVideoFormats is running.'); + } + expect(isOnError).toBeTruthy(); + }); + + test('error convertVideoFormats', async () => { + const inUrl = 'http://192.168.1.1/files/100RICOH/R0013330.MP4'; + const toLowResolution = true; + const applyTopBottomCorrection = true; + jest.mocked(thetaClient.convertVideoFormats).mockImplementation( + jest.fn(async () => { + throw 'error'; + }) + ); + + let isOnError = false; + try { + await convertVideoFormats( + inUrl, + toLowResolution, + applyTopBottomCorrection + ); + expect(false).toBeTruthy(); + } catch (error) { + isOnError = true; + expect(error).toBe('error'); + } + expect(isOnError).toBeTruthy(); + }); +}); diff --git a/react-native/src/theta-repository/libs/convert-video-formats.ts b/react-native/src/theta-repository/libs/convert-video-formats.ts new file mode 100644 index 0000000000..c560fa9a58 --- /dev/null +++ b/react-native/src/theta-repository/libs/convert-video-formats.ts @@ -0,0 +1,41 @@ +import { NativeModules } from 'react-native'; +import { BaseNotify, NotifyController } from '../notify-controller'; +const ThetaClientReactNative = NativeModules.ThetaClientReactNative; + +const NOTIFY_PROGRESS = 'CONVERT-VIDEO-FORMATS-PROGRESS'; +const MESSAGE_ERROR_RUNNING = 'convertVideoFormats is running.'; + +interface ProgressNotify extends BaseNotify { + params?: { + completion: number; + }; +} + +export async function convertVideoFormatsImpl( + fileUrl: string, + toLowResolution: boolean, + applyTopBottomCorrection: boolean, + onProgress?: (completion: number) => void +): Promise { + const notify = NotifyController.instance; + if (notify.existsNotify(NOTIFY_PROGRESS)) { + throw new Error(MESSAGE_ERROR_RUNNING); + } + notify.addNotify(NOTIFY_PROGRESS, (event: ProgressNotify) => { + if (onProgress != null && event.params?.completion != null) { + onProgress(event.params.completion); + } + }); + try { + const result: string = await ThetaClientReactNative.convertVideoFormats( + fileUrl, + toLowResolution, + applyTopBottomCorrection + ); + return result; + } catch (error: any) { + throw error; + } finally { + notify.removeNotify(NOTIFY_PROGRESS); + } +} diff --git a/react-native/src/theta-repository/libs/index.ts b/react-native/src/theta-repository/libs/index.ts new file mode 100644 index 0000000000..881f73cdb1 --- /dev/null +++ b/react-native/src/theta-repository/libs/index.ts @@ -0,0 +1 @@ +export * from './convert-video-formats'; diff --git a/react-native/src/theta-repository/notify-controller.ts b/react-native/src/theta-repository/notify-controller.ts index c55ac3dd2b..7161f986e2 100644 --- a/react-native/src/theta-repository/notify-controller.ts +++ b/react-native/src/theta-repository/notify-controller.ts @@ -39,6 +39,10 @@ export class NotifyController { this.notifyList.delete(name); } + existsNotify(name: string): boolean { + return this.notifyList.has(name); + } + private addNotifyListener( callback: (event: BaseNotify) => void ): EmitterSubscription { diff --git a/react-native/src/theta-repository/theta-repository.ts b/react-native/src/theta-repository/theta-repository.ts index ac39b6d51f..51076d742e 100644 --- a/react-native/src/theta-repository/theta-repository.ts +++ b/react-native/src/theta-repository/theta-repository.ts @@ -33,6 +33,7 @@ import type { ThetaConfig } from './theta-config'; import type { ThetaTimeout } from './theta-timeout'; import { NotifyController } from './notify-controller'; import { EventWebSocket } from './event-websocket'; +import { convertVideoFormatsImpl } from './libs'; const ThetaClientReactNative = NativeModules.ThetaClientReactNative; /** @@ -239,17 +240,20 @@ export function stopSelfTimer(): Promise { * video, otherwise same resolution. * @param {boolean} applyTopBottomCorrection apply Top/bottom * correction. This parameter is ignored on Theta X. + * @param onProgress the block for convertVideoFormats progress * @return promise of URL of a converted movie file. */ export function convertVideoFormats( fileUrl: string, toLowResolution: boolean, - applyTopBottomCorrection: boolean + applyTopBottomCorrection: boolean, + onProgress?: (completion: number) => void ): Promise { - return ThetaClientReactNative.convertVideoFormats( + return convertVideoFormatsImpl( fileUrl, toLowResolution, - applyTopBottomCorrection + applyTopBottomCorrection, + onProgress ); } diff --git a/react-native/verification-tool/src/screen/video-convert-screen/video-convert-screen.tsx b/react-native/verification-tool/src/screen/video-convert-screen/video-convert-screen.tsx index 515c3b183e..52545472c6 100644 --- a/react-native/verification-tool/src/screen/video-convert-screen/video-convert-screen.tsx +++ b/react-native/verification-tool/src/screen/video-convert-screen/video-convert-screen.tsx @@ -51,7 +51,10 @@ const VideoConvertScreen: React.FC< const result = await convertVideoFormats( fileUrl, toLowResolution, - applyTopBottomCorrection + applyTopBottomCorrection, + (completion) => { + setMessage(`onProgress: ${completion}`); + } ); setMessage('convertVideoFormats\nEnd convert.\nfileUrl:\n' + result); } catch (error) { From b7fddd8501ccb042e14fd485c59472fb805baee9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=BE=E5=B2=A1=E3=80=80=E4=BE=91=E5=87=9B?= <129148471+LassicYM@users.noreply.github.com> Date: Mon, 3 Feb 2025 13:42:24 +0900 Subject: [PATCH 2/3] add 4K 30fps into previewFormat. --- flutter/lib/theta_client_flutter.dart | 6 +++++- flutter/test/enum_name_test.dart | 1 + .../kotlin/com/ricoh360/thetaclient/ThetaRepository.kt | 3 ++- .../thetaclient/repository/options/PreviewFormatTest.kt | 1 + .../src/__tests__/options/option-preview-format.test.tsx | 1 + .../src/theta-repository/options/option-preview-format.ts | 2 ++ 6 files changed, 12 insertions(+), 2 deletions(-) diff --git a/flutter/lib/theta_client_flutter.dart b/flutter/lib/theta_client_flutter.dart index b386bd6db6..d875280e47 100644 --- a/flutter/lib/theta_client_flutter.dart +++ b/flutter/lib/theta_client_flutter.dart @@ -2487,7 +2487,11 @@ enum PreviewFormatEnum { /// For Theta S and SC // ignore: constant_identifier_names - w640_h320_f10('W640_H320_F10'); + w640_h320_f10('W640_H320_F10'), + + /// For Theta X + // ignore: constant_identifier_names + w3840_h1920_f30('W3840_H1920_F30'); final String rawValue; diff --git a/flutter/test/enum_name_test.dart b/flutter/test/enum_name_test.dart index f1984bcbae..33ab264ca6 100644 --- a/flutter/test/enum_name_test.dart +++ b/flutter/test/enum_name_test.dart @@ -751,6 +751,7 @@ void main() { [PreviewFormatEnum.w640_h320_f30, 'W640_H320_F30'], [PreviewFormatEnum.w640_h320_f8, 'W640_H320_F8'], [PreviewFormatEnum.w640_h320_f10, 'W640_H320_F10'], + [PreviewFormatEnum.w3840_h1920_f30, 'W3840_H1920_F30'], ]; expect(data.length, PreviewFormatEnum.values.length, reason: 'enum count'); for (int i = 0; i < data.length; i++) { 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 5e870965db..0a3b9015cf 100644 --- a/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/ThetaRepository.kt +++ b/kotlin-multiplatform/src/commonMain/kotlin/com/ricoh360/thetaclient/ThetaRepository.kt @@ -5065,7 +5065,8 @@ class ThetaRepository internal constructor(val endpoint: String, config: Config? W1024_H512_F8(1024, 512, 8), // For Theta Z1 and V W640_H320_F30(640, 320, 30), // For Theta Z1 and V W640_H320_F8(640, 320, 8), // For Theta Z1 and V - W640_H320_F10(640, 320, 10); // For Theta S and SC + W640_H320_F10(640, 320, 10), // For Theta S and SC + W3840_H1920_F30(3840, 1920, 30); // For Theta X /** * Convert PreviewFormatEnum to PreviewFormat. diff --git a/kotlin-multiplatform/src/commonTest/kotlin/com/ricoh360/thetaclient/repository/options/PreviewFormatTest.kt b/kotlin-multiplatform/src/commonTest/kotlin/com/ricoh360/thetaclient/repository/options/PreviewFormatTest.kt index 27b636b74a..9ef3e01852 100644 --- a/kotlin-multiplatform/src/commonTest/kotlin/com/ricoh360/thetaclient/repository/options/PreviewFormatTest.kt +++ b/kotlin-multiplatform/src/commonTest/kotlin/com/ricoh360/thetaclient/repository/options/PreviewFormatTest.kt @@ -95,6 +95,7 @@ class PreviewFormatTest { Pair(ThetaRepository.PreviewFormatEnum.W640_H320_F30, PreviewFormat(640, 320, 30)), Pair(ThetaRepository.PreviewFormatEnum.W640_H320_F10, PreviewFormat(640, 320, 10)), Pair(ThetaRepository.PreviewFormatEnum.W640_H320_F8, PreviewFormat(640, 320, 8)), + Pair(ThetaRepository.PreviewFormatEnum.W3840_H1920_F30, PreviewFormat(3840, 1920, 30)), ) values.forEach { diff --git a/react-native/src/__tests__/options/option-preview-format.test.tsx b/react-native/src/__tests__/options/option-preview-format.test.tsx index 7e2e6d8484..5d140ee9c7 100644 --- a/react-native/src/__tests__/options/option-preview-format.test.tsx +++ b/react-native/src/__tests__/options/option-preview-format.test.tsx @@ -10,6 +10,7 @@ describe('PreviewFormatEnum', () => { [PreviewFormatEnum.W640_H320_F30, 'W640_H320_F30'], [PreviewFormatEnum.W640_H320_F8, 'W640_H320_F8'], [PreviewFormatEnum.W640_H320_F10, 'W640_H320_F10'], + [PreviewFormatEnum.W3840_H1920_F30, 'W3840_H1920_F30'], ]; test('length', () => { diff --git a/react-native/src/theta-repository/options/option-preview-format.ts b/react-native/src/theta-repository/options/option-preview-format.ts index 70e0d462c6..f8df494b19 100644 --- a/react-native/src/theta-repository/options/option-preview-format.ts +++ b/react-native/src/theta-repository/options/option-preview-format.ts @@ -19,6 +19,8 @@ export const PreviewFormatEnum = { W640_H320_F8: 'W640_H320_F8', /** For Theta S and SC */ W640_H320_F10: 'W640_H320_F10', + /** For Theta X */ + W3840_H1920_F30: 'W3840_H1920_F30', } as const; /** Type definition of PreviewFormatEnum */ From cceebf818f5d4d8bfb7f76a3f3f0a31dae85a909 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=BE=E5=B2=A1=E3=80=80=E4=BE=91=E5=87=9B?= <129148471+LassicYM@users.noreply.github.com> Date: Mon, 3 Feb 2025 14:25:43 +0900 Subject: [PATCH 3/3] Update version to 1.12.0 --- demos/demo-android/app/build.gradle | 2 +- demos/demo-ios/Podfile | 2 +- demos/demo-react-native/package.json | 2 +- docs/tutorial-android.ja.md | 2 +- docs/tutorial-android.md | 2 +- flutter/android/build.gradle | 2 +- flutter/ios/theta_client_flutter.podspec | 4 ++-- flutter/pubspec.yaml | 2 +- kotlin-multiplatform/build.gradle.kts | 2 +- react-native/android/build.gradle | 2 +- react-native/package.json | 2 +- react-native/theta-client-react-native.podspec | 2 +- 12 files changed, 13 insertions(+), 13 deletions(-) diff --git a/demos/demo-android/app/build.gradle b/demos/demo-android/app/build.gradle index 66dd667389..95a70b5401 100755 --- a/demos/demo-android/app/build.gradle +++ b/demos/demo-android/app/build.gradle @@ -75,7 +75,7 @@ dependencies { implementation 'com.jakewharton.timber:timber:5.0.1' implementation 'io.coil-kt:coil-compose:2.2.2' implementation "io.ktor:ktor-client-cio:2.3.9" - implementation "com.ricoh360.thetaclient:theta-client:1.11.1" + implementation "com.ricoh360.thetaclient:theta-client:1.12.0" testImplementation 'org.junit.jupiter:junit-jupiter:5.9.0' testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version" diff --git a/demos/demo-ios/Podfile b/demos/demo-ios/Podfile index daeda5bd64..ec07b34191 100644 --- a/demos/demo-ios/Podfile +++ b/demos/demo-ios/Podfile @@ -7,5 +7,5 @@ target 'SdkSample' do use_frameworks! # Pods for SdkSample - pod 'THETAClient', '1.11.1' + pod 'THETAClient', '1.12.0' end diff --git a/demos/demo-react-native/package.json b/demos/demo-react-native/package.json index 34bc524d0e..d702a7b061 100644 --- a/demos/demo-react-native/package.json +++ b/demos/demo-react-native/package.json @@ -13,7 +13,7 @@ "dependencies": { "@react-navigation/native": "^6.1.0", "@react-navigation/native-stack": "^6.9.5", - "theta-client-react-native": "1.11.1", + "theta-client-react-native": "1.12.0", "react": "18.2.0", "react-native": "0.71.14", "react-native-safe-area-context": "^4.4.1", diff --git a/docs/tutorial-android.ja.md b/docs/tutorial-android.ja.md index eb392d6de7..88406ee1fb 100644 --- a/docs/tutorial-android.ja.md +++ b/docs/tutorial-android.ja.md @@ -4,7 +4,7 @@ - モジュールの`build.gradle`の`dependencies`に次を追加します。 ``` - implementation "com.ricoh360.thetaclient:theta-client:1.11.1" + implementation "com.ricoh360.thetaclient:theta-client:1.12.0" ``` - 本 SDK を使用したアプリケーションが動作するスマートフォンと THETA を無線 LAN 接続しておきます。 diff --git a/docs/tutorial-android.md b/docs/tutorial-android.md index b4950fe165..634ab66a79 100644 --- a/docs/tutorial-android.md +++ b/docs/tutorial-android.md @@ -5,7 +5,7 @@ - Add following descriptions to the `dependencies` of your module's `build.gradle`. ``` - implementation "com.ricoh360.thetaclient:theta-client:1.11.1" + implementation "com.ricoh360.thetaclient:theta-client:1.12.0" ``` - Connect the wireless LAN between THETA and the smartphone that runs on the application using this SDK. diff --git a/flutter/android/build.gradle b/flutter/android/build.gradle index 1720919913..afbb1c4ba3 100644 --- a/flutter/android/build.gradle +++ b/flutter/android/build.gradle @@ -53,5 +53,5 @@ dependencies { implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.9") implementation("com.soywiz.korlibs.krypto:krypto:4.0.10") - implementation("com.ricoh360.thetaclient:theta-client:1.11.1") + implementation("com.ricoh360.thetaclient:theta-client:1.12.0") } diff --git a/flutter/ios/theta_client_flutter.podspec b/flutter/ios/theta_client_flutter.podspec index 9bd43993a5..8a57f10b28 100644 --- a/flutter/ios/theta_client_flutter.podspec +++ b/flutter/ios/theta_client_flutter.podspec @@ -4,7 +4,7 @@ # Pod::Spec.new do |s| s.name = 'theta_client_flutter' - s.version = '1.11.1' + s.version = '1.12.0' s.summary = 'theta-client plugin project.' s.description = <<-DESC theta-client Flutter plugin project. @@ -17,7 +17,7 @@ Pod::Spec.new do |s| s.dependency 'Flutter' s.platform = :ios, '15.0' - s.dependency 'THETAClient', '1.11.1' + s.dependency 'THETAClient', '1.12.0' # Flutter.framework does not contain a i386 slice. s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index fb8d636a3a..1715dfcdfd 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -1,6 +1,6 @@ name: theta_client_flutter description: THETA Client Flutter plugin project. -version: 1.11.1 +version: 1.12.0 homepage: environment: diff --git a/kotlin-multiplatform/build.gradle.kts b/kotlin-multiplatform/build.gradle.kts index 7bf01659bc..3753afede3 100644 --- a/kotlin-multiplatform/build.gradle.kts +++ b/kotlin-multiplatform/build.gradle.kts @@ -17,7 +17,7 @@ dependencies { dokkaPlugin("org.jetbrains.dokka:versioning-plugin:1.9.10") } -val thetaClientVersion = "1.11.1" +val thetaClientVersion = "1.12.0" group = "com.ricoh360.thetaclient" version = thetaClientVersion diff --git a/react-native/android/build.gradle b/react-native/android/build.gradle index de9f812967..181c981c7d 100644 --- a/react-native/android/build.gradle +++ b/react-native/android/build.gradle @@ -135,7 +135,7 @@ dependencies { implementation "com.facebook.react:react-native:+" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3" - implementation "com.ricoh360.thetaclient:theta-client:1.11.1" + implementation "com.ricoh360.thetaclient:theta-client:1.12.0" // From node_modules } diff --git a/react-native/package.json b/react-native/package.json index 73cfa700d4..d503c21d14 100644 --- a/react-native/package.json +++ b/react-native/package.json @@ -1,6 +1,6 @@ { "name": "theta-client-react-native", - "version": "1.11.1", + "version": "1.12.0", "description": "This library provides a way to control RICOH THETA using.", "main": "lib/commonjs/index", "module": "lib/module/index", diff --git a/react-native/theta-client-react-native.podspec b/react-native/theta-client-react-native.podspec index 5cf87a75fb..1446f7c86b 100644 --- a/react-native/theta-client-react-native.podspec +++ b/react-native/theta-client-react-native.podspec @@ -17,7 +17,7 @@ Pod::Spec.new do |s| s.source_files = "ios/**/*.{h,m,mm,swift}" s.dependency "React-Core" - s.dependency "THETAClient", "1.11.1" + s.dependency "THETAClient", "1.12.0" # Don't install the dependencies when we run `pod install` in the old architecture. if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then