From 4347cefaed25aef97effa46dc421eecc9a220011 Mon Sep 17 00:00:00 2001 From: JulianAlcala Date: Thu, 16 Apr 2026 17:45:33 +0200 Subject: [PATCH] feat(videocall): add videocall_sdk package wrapping Juphoon jc_sdk Full wrapper around jc_sdk v2.16.5 with clean architecture: - 7 services covering 100% of jc_sdk public API (Client, Call, Device, Channel, Push, Net, Log) - Constructor injection with GetIt DI module (follows sca_treezor pattern) - VideocallSdkManager orchestrator for init/destroy lifecycle - VideocallSdkConfig abstract for environment-specific AppKey - Stream-based callbacks for reactive UI consumption - Riverpod providers (service + stream) for feature layer - AppKey configured per environment via dart-define-from-file - Integrated in init_app.dart alongside scaTreezorModule --- apps/mobile_app/config/development.json | 3 +- apps/mobile_app/config/production.json | 5 +- apps/mobile_app/config/staging.json | 3 +- .../lib/config/env/environment.dart | 1 + .../env/save_family_videocall_config.dart | 14 + apps/mobile_app/lib/core/init_app.dart | 3 + apps/mobile_app/pubspec.yaml | 2 + packages/videocall_sdk/.gitignore | 31 ++ packages/videocall_sdk/CHANGELOG.md | 3 + packages/videocall_sdk/LICENSE | 1 + packages/videocall_sdk/README.md | 5 + packages/videocall_sdk/analysis_options.yaml | 4 + .../lib/src/config/videocall_sdk_config.dart | 7 + .../lib/src/di/videocall_sdk_module.dart | 65 +++ .../src/manager/videocall_sdk_manager.dart | 75 ++++ .../lib/src/models/call_direction.dart | 4 + .../lib/src/models/call_state.dart | 9 + .../lib/src/models/login_failure_reason.dart | 12 + .../src/models/videocall_client_state.dart | 7 + .../lib/src/models/videocall_item.dart | 30 ++ .../src/providers/videocall_providers.dart | 62 +++ .../src/services/videocall_call_service.dart | 313 ++++++++++++++ .../services/videocall_channel_service.dart | 399 ++++++++++++++++++ .../lib/src/services/videocall_client.dart | 247 +++++++++++ .../services/videocall_device_service.dart | 394 +++++++++++++++++ .../src/services/videocall_log_service.dart | 13 + .../src/services/videocall_net_service.dart | 42 ++ .../src/services/videocall_push_service.dart | 45 ++ packages/videocall_sdk/lib/videocall_sdk.dart | 60 +++ packages/videocall_sdk/pubspec.yaml | 21 + pubspec.lock | 8 + pubspec.yaml | 1 + 32 files changed, 1885 insertions(+), 4 deletions(-) create mode 100644 apps/mobile_app/lib/config/env/save_family_videocall_config.dart create mode 100644 packages/videocall_sdk/.gitignore create mode 100644 packages/videocall_sdk/CHANGELOG.md create mode 100644 packages/videocall_sdk/LICENSE create mode 100644 packages/videocall_sdk/README.md create mode 100644 packages/videocall_sdk/analysis_options.yaml create mode 100644 packages/videocall_sdk/lib/src/config/videocall_sdk_config.dart create mode 100644 packages/videocall_sdk/lib/src/di/videocall_sdk_module.dart create mode 100644 packages/videocall_sdk/lib/src/manager/videocall_sdk_manager.dart create mode 100644 packages/videocall_sdk/lib/src/models/call_direction.dart create mode 100644 packages/videocall_sdk/lib/src/models/call_state.dart create mode 100644 packages/videocall_sdk/lib/src/models/login_failure_reason.dart create mode 100644 packages/videocall_sdk/lib/src/models/videocall_client_state.dart create mode 100644 packages/videocall_sdk/lib/src/models/videocall_item.dart create mode 100644 packages/videocall_sdk/lib/src/providers/videocall_providers.dart create mode 100644 packages/videocall_sdk/lib/src/services/videocall_call_service.dart create mode 100644 packages/videocall_sdk/lib/src/services/videocall_channel_service.dart create mode 100644 packages/videocall_sdk/lib/src/services/videocall_client.dart create mode 100644 packages/videocall_sdk/lib/src/services/videocall_device_service.dart create mode 100644 packages/videocall_sdk/lib/src/services/videocall_log_service.dart create mode 100644 packages/videocall_sdk/lib/src/services/videocall_net_service.dart create mode 100644 packages/videocall_sdk/lib/src/services/videocall_push_service.dart create mode 100644 packages/videocall_sdk/lib/videocall_sdk.dart create mode 100644 packages/videocall_sdk/pubspec.yaml diff --git a/apps/mobile_app/config/development.json b/apps/mobile_app/config/development.json index 3b621bd6..9c4863ac 100644 --- a/apps/mobile_app/config/development.json +++ b/apps/mobile_app/config/development.json @@ -2,5 +2,6 @@ "env": "development", "apiBaseUrl": "https://api-neki-b2b.neki.es/gateway/api/", "apiOrigin": "https://neki-b2b.neki.es", - "wsUrl": "wss://api-neki-b2b.neki.es/websocket" + "wsUrl": "wss://api-neki-b2b.neki.es/websocket", + "juphoonAppKey": "9efcf2d889dc8a0320925096" } \ No newline at end of file diff --git a/apps/mobile_app/config/production.json b/apps/mobile_app/config/production.json index f646603f..d6b3934f 100644 --- a/apps/mobile_app/config/production.json +++ b/apps/mobile_app/config/production.json @@ -2,5 +2,6 @@ "env": "production", "apiBaseUrl": "https://api-platform.savefamily.app/gateway/api/", "apiOrigin": "https://platform.savefamily.app", - "wsUrl": "wss://api-platform.savefamily.app/websocket" -} \ No newline at end of file + "wsUrl": "wss://api-platform.savefamily.app/websocket", + "juphoonAppKey": "9efcf2d889dc8a0320925096" +} diff --git a/apps/mobile_app/config/staging.json b/apps/mobile_app/config/staging.json index 40e8138d..d20530de 100644 --- a/apps/mobile_app/config/staging.json +++ b/apps/mobile_app/config/staging.json @@ -2,5 +2,6 @@ "env": "staging", "apiBaseUrl": "https://api-platform.pre.savefamilygps.net/gateway/api/", "apiOrigin": "https://platform.pre.savefamilygps.net", - "wsUrl": "wss://api-platform.pre.savefamilygps.net/websocket" + "wsUrl": "wss://api-platform.pre.savefamilygps.net/websocket", + "juphoonAppKey": "9efcf2d889dc8a0320925096" } \ No newline at end of file diff --git a/apps/mobile_app/lib/config/env/environment.dart b/apps/mobile_app/lib/config/env/environment.dart index 9c87ad24..8e2d7e64 100644 --- a/apps/mobile_app/lib/config/env/environment.dart +++ b/apps/mobile_app/lib/config/env/environment.dart @@ -3,6 +3,7 @@ abstract class Environment { static const apiBaseUrl = String.fromEnvironment('apiBaseUrl'); static const apiOrigin = String.fromEnvironment('apiOrigin'); static const wsUrl = String.fromEnvironment('wsUrl'); + static const juphoonAppKey = String.fromEnvironment('juphoonAppKey'); // --- Fase 2: Firebase & Sentry --- // static const sentryDsn = String.fromEnvironment('sentryDsn'); diff --git a/apps/mobile_app/lib/config/env/save_family_videocall_config.dart b/apps/mobile_app/lib/config/env/save_family_videocall_config.dart new file mode 100644 index 00000000..3ea6dc7d --- /dev/null +++ b/apps/mobile_app/lib/config/env/save_family_videocall_config.dart @@ -0,0 +1,14 @@ +import 'package:videocall_sdk/videocall_sdk.dart'; + +import 'environment.dart'; + +class SaveFamilyVideocallConfig implements VideocallSdkConfig { + @override + String get appKey => Environment.juphoonAppKey; + + @override + String get serverAddress => ''; + + @override + CreateParam? get createParam => null; +} diff --git a/apps/mobile_app/lib/core/init_app.dart b/apps/mobile_app/lib/core/init_app.dart index 2401c8f7..42f05931 100644 --- a/apps/mobile_app/lib/core/init_app.dart +++ b/apps/mobile_app/lib/core/init_app.dart @@ -6,6 +6,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/date_symbol_data_local.dart'; import 'package:design_system/design_system.dart'; import 'package:sca_treezor/sca_treezor.dart'; +import 'package:videocall_sdk/videocall_sdk.dart'; +import 'package:sf_app_platform/config/env/save_family_videocall_config.dart'; import 'package:sf_app_platform/config/env/environment_enum.dart'; import 'package:sf_app_platform/config/env/save_family_env_config.dart'; import 'package:sf_app_platform/core/config/app_mode.dart'; @@ -24,6 +26,7 @@ Future initApp(EnvironmentEnum env) async { navigationModule(); scaTreezorModule(); + videocallSdkModule(SaveFamilyVideocallConfig()); themePackages(); await setupFirebase(env); diff --git a/apps/mobile_app/pubspec.yaml b/apps/mobile_app/pubspec.yaml index fade1815..812f070e 100644 --- a/apps/mobile_app/pubspec.yaml +++ b/apps/mobile_app/pubspec.yaml @@ -91,6 +91,8 @@ dependencies: path: ../../packages/sca_treezor payments: path: ../../packages/payments + videocall_sdk: + path: ../../packages/videocall_sdk #dependencies go here cupertino_icons: ^1.0.8 flutter_svg: ^2.2.2 diff --git a/packages/videocall_sdk/.gitignore b/packages/videocall_sdk/.gitignore new file mode 100644 index 00000000..dd5eb989 --- /dev/null +++ b/packages/videocall_sdk/.gitignore @@ -0,0 +1,31 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.flutter-plugins-dependencies +/build/ +/coverage/ diff --git a/packages/videocall_sdk/CHANGELOG.md b/packages/videocall_sdk/CHANGELOG.md new file mode 100644 index 00000000..619a3880 --- /dev/null +++ b/packages/videocall_sdk/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* Initial release. Wrapper around Juphoon jc_sdk for video calling. diff --git a/packages/videocall_sdk/LICENSE b/packages/videocall_sdk/LICENSE new file mode 100644 index 00000000..ba75c69f --- /dev/null +++ b/packages/videocall_sdk/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/packages/videocall_sdk/README.md b/packages/videocall_sdk/README.md new file mode 100644 index 00000000..c9a8254f --- /dev/null +++ b/packages/videocall_sdk/README.md @@ -0,0 +1,5 @@ +## videocall_sdk + +Wrapper around Juphoon jc_sdk for video calling in SaveFamily. + +Provides a clean Dart API over the native Juphoon SDK, isolating the dependency from the rest of the app. diff --git a/packages/videocall_sdk/analysis_options.yaml b/packages/videocall_sdk/analysis_options.yaml new file mode 100644 index 00000000..a5744c1c --- /dev/null +++ b/packages/videocall_sdk/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/packages/videocall_sdk/lib/src/config/videocall_sdk_config.dart b/packages/videocall_sdk/lib/src/config/videocall_sdk_config.dart new file mode 100644 index 00000000..20c3df4a --- /dev/null +++ b/packages/videocall_sdk/lib/src/config/videocall_sdk_config.dart @@ -0,0 +1,7 @@ +import 'package:jc_sdk/jc_sdk.dart'; + +abstract class VideocallSdkConfig { + String get appKey; + String get serverAddress; + CreateParam? get createParam; +} diff --git a/packages/videocall_sdk/lib/src/di/videocall_sdk_module.dart b/packages/videocall_sdk/lib/src/di/videocall_sdk_module.dart new file mode 100644 index 00000000..fd33c171 --- /dev/null +++ b/packages/videocall_sdk/lib/src/di/videocall_sdk_module.dart @@ -0,0 +1,65 @@ +import 'package:get_it/get_it.dart'; + +import '../config/videocall_sdk_config.dart'; +import '../manager/videocall_sdk_manager.dart'; +import '../services/videocall_call_service.dart'; +import '../services/videocall_channel_service.dart'; +import '../services/videocall_client.dart'; +import '../services/videocall_device_service.dart'; +import '../services/videocall_net_service.dart'; +import '../services/videocall_push_service.dart'; + +void videocallSdkModule(VideocallSdkConfig config) { + final getIt = GetIt.instance; + + // Config + getIt.registerSingleton(config); + + // Core client + getIt.registerLazySingleton( + () => VideocallClient(getIt()), + ); + + // Device (depends on Client) + getIt.registerLazySingleton( + () => VideocallDeviceService(client: getIt()), + ); + + // Call (depends on Client + Device) + getIt.registerLazySingleton( + () => VideocallCallService( + client: getIt(), + deviceService: getIt(), + ), + ); + + // Channel (depends on Client + Device) + getIt.registerLazySingleton( + () => VideocallChannelService( + client: getIt(), + deviceService: getIt(), + ), + ); + + // Push (depends on Client) + getIt.registerLazySingleton( + () => VideocallPushService(client: getIt()), + ); + + // Net (standalone) + getIt.registerLazySingleton( + () => VideocallNetService(), + ); + + // Manager (orchestrator) + getIt.registerLazySingleton( + () => VideocallSdkManager( + client: getIt(), + deviceService: getIt(), + callService: getIt(), + channelService: getIt(), + pushService: getIt(), + netService: getIt(), + ), + ); +} diff --git a/packages/videocall_sdk/lib/src/manager/videocall_sdk_manager.dart b/packages/videocall_sdk/lib/src/manager/videocall_sdk_manager.dart new file mode 100644 index 00000000..4836c87b --- /dev/null +++ b/packages/videocall_sdk/lib/src/manager/videocall_sdk_manager.dart @@ -0,0 +1,75 @@ +import '../services/videocall_call_service.dart'; +import '../services/videocall_channel_service.dart'; +import '../services/videocall_client.dart'; +import '../services/videocall_device_service.dart'; +import '../services/videocall_net_service.dart'; +import '../services/videocall_push_service.dart'; + +class VideocallSdkManager { + VideocallSdkManager({ + required this.client, + required this.deviceService, + required this.callService, + required this.channelService, + required this.pushService, + required this.netService, + }); + + final VideocallClient client; + final VideocallDeviceService deviceService; + final VideocallCallService callService; + final VideocallChannelService channelService; + final VideocallPushService pushService; + final VideocallNetService netService; + + bool _initialized = false; + bool get isInitialized => _initialized; + + Future initialize() async { + if (_initialized) return true; + + // Phase 1: Client (no deps) + final clientOk = await client.initialize(); + if (!clientOk) return false; + + // Phase 2: Device + Net (depend on Client only) + final deviceOk = await deviceService.initialize(); + if (!deviceOk) return false; + netService.initialize(); + + // Phase 3: Call + Channel + Push (depend on Client + Device) + final phase3 = await Future.wait([ + callService.initialize(), + channelService.initialize(), + pushService.initialize(), + ]); + if (!phase3.every((r) => r)) return false; + + _initialized = true; + return true; + } + + Future destroy() async { + if (!_initialized) return; + + // Reverse order + await pushService.destroy(); + await channelService.destroy(); + await callService.destroy(); + netService.uninitialize(); + await deviceService.destroy(); + await client.destroy(); + + _initialized = false; + } + + void dispose() { + callService.dispose(); + channelService.dispose(); + pushService.dispose(); + netService.dispose(); + deviceService.dispose(); + client.dispose(); + _initialized = false; + } +} diff --git a/packages/videocall_sdk/lib/src/models/call_direction.dart b/packages/videocall_sdk/lib/src/models/call_direction.dart new file mode 100644 index 00000000..91ccf29d --- /dev/null +++ b/packages/videocall_sdk/lib/src/models/call_direction.dart @@ -0,0 +1,4 @@ +enum CallDirection { + incoming, + outgoing, +} diff --git a/packages/videocall_sdk/lib/src/models/call_state.dart b/packages/videocall_sdk/lib/src/models/call_state.dart new file mode 100644 index 00000000..7602138c --- /dev/null +++ b/packages/videocall_sdk/lib/src/models/call_state.dart @@ -0,0 +1,9 @@ +enum VideocallState { + idle, + pending, + connecting, + talking, + ok, + canceled, + missed, +} diff --git a/packages/videocall_sdk/lib/src/models/login_failure_reason.dart b/packages/videocall_sdk/lib/src/models/login_failure_reason.dart new file mode 100644 index 00000000..46d60f52 --- /dev/null +++ b/packages/videocall_sdk/lib/src/models/login_failure_reason.dart @@ -0,0 +1,12 @@ +enum LoginFailureReason { + auth, + network, + appkey, + noUser, + timeout, + serverLogout, + anotherDeviceLoggedIn, + tokenMismatch, + tokenExpired, + unknown, +} diff --git a/packages/videocall_sdk/lib/src/models/videocall_client_state.dart b/packages/videocall_sdk/lib/src/models/videocall_client_state.dart new file mode 100644 index 00000000..264d6e05 --- /dev/null +++ b/packages/videocall_sdk/lib/src/models/videocall_client_state.dart @@ -0,0 +1,7 @@ +enum VideocallClientState { + notInitialized, + idle, + loggingIn, + loggedIn, + loggingOut, +} diff --git a/packages/videocall_sdk/lib/src/models/videocall_item.dart b/packages/videocall_sdk/lib/src/models/videocall_item.dart new file mode 100644 index 00000000..9705719b --- /dev/null +++ b/packages/videocall_sdk/lib/src/models/videocall_item.dart @@ -0,0 +1,30 @@ +import 'call_direction.dart'; +import 'call_state.dart'; + +class VideocallItem { + const VideocallItem({ + required this.userId, + required this.isVideo, + required this.direction, + required this.state, + }); + + final String userId; + final bool isVideo; + final CallDirection direction; + final VideocallState state; + + VideocallItem copyWith({ + String? userId, + bool? isVideo, + CallDirection? direction, + VideocallState? state, + }) { + return VideocallItem( + userId: userId ?? this.userId, + isVideo: isVideo ?? this.isVideo, + direction: direction ?? this.direction, + state: state ?? this.state, + ); + } +} diff --git a/packages/videocall_sdk/lib/src/providers/videocall_providers.dart b/packages/videocall_sdk/lib/src/providers/videocall_providers.dart new file mode 100644 index 00000000..ba1ef12b --- /dev/null +++ b/packages/videocall_sdk/lib/src/providers/videocall_providers.dart @@ -0,0 +1,62 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:get_it/get_it.dart'; + +import '../manager/videocall_sdk_manager.dart'; +import '../models/videocall_client_state.dart'; +import '../models/videocall_item.dart'; +import '../services/videocall_call_service.dart'; +import '../services/videocall_channel_service.dart'; +import '../services/videocall_client.dart'; +import '../services/videocall_device_service.dart'; +import '../services/videocall_net_service.dart'; +import '../services/videocall_push_service.dart'; + +// -- Service providers (thin wrappers over GetIt) -- + +final videocallManagerProvider = Provider((ref) { + return GetIt.I(); +}); + +final videocallClientProvider = Provider((ref) { + return GetIt.I(); +}); + +final videocallCallServiceProvider = Provider((ref) { + return GetIt.I(); +}); + +final videocallDeviceServiceProvider = Provider((ref) { + return GetIt.I(); +}); + +final videocallChannelServiceProvider = + Provider((ref) { + return GetIt.I(); +}); + +final videocallPushServiceProvider = Provider((ref) { + return GetIt.I(); +}); + +final videocallNetServiceProvider = Provider((ref) { + return GetIt.I(); +}); + +// -- Stream providers (for reactive UI consumption) -- + +final videocallClientStateProvider = + StreamProvider((ref) { + return ref.watch(videocallClientProvider).stateStream; +}); + +final videocallCurrentCallProvider = StreamProvider((ref) { + final callService = ref.watch(videocallCallServiceProvider); + + Stream stream() async* { + yield callService.currentItem; + yield* callService.callItemUpdateStream + .map((_) => callService.currentItem); + } + + return stream(); +}); diff --git a/packages/videocall_sdk/lib/src/services/videocall_call_service.dart b/packages/videocall_sdk/lib/src/services/videocall_call_service.dart new file mode 100644 index 00000000..3f23967d --- /dev/null +++ b/packages/videocall_sdk/lib/src/services/videocall_call_service.dart @@ -0,0 +1,313 @@ +import 'dart:async'; + +import 'package:jc_sdk/jc_sdk.dart'; + +import '../models/call_direction.dart'; +import '../models/call_state.dart'; +import '../models/videocall_item.dart'; +import 'videocall_client.dart'; +import 'videocall_device_service.dart'; + +class VideocallCallService with JCCallCallback { + VideocallCallService({ + required VideocallClient client, + required VideocallDeviceService deviceService, + }) : _clientRef = client, + _deviceRef = deviceService; + + final VideocallClient _clientRef; + final VideocallDeviceService _deviceRef; + JCCall? _call; + JCCall? get call => _call; + + // -- Streams -- + + final _callItemAddController = StreamController.broadcast(); + Stream get callItemAddStream => _callItemAddController.stream; + + final _callItemUpdateController = StreamController.broadcast(); + Stream get callItemUpdateStream => + _callItemUpdateController.stream; + + final _callItemRemoveController = + StreamController<({int reason, String description})>.broadcast(); + Stream<({int reason, String description})> get callItemRemoveStream => + _callItemRemoveController.stream; + + final _missedCallController = StreamController.broadcast(); + Stream get missedCallStream => _missedCallController.stream; + + final _messageReceivedController = + StreamController<({String type, String content, JCCallItem item})> + .broadcast(); + Stream<({String type, String content, JCCallItem item})> + get messageReceivedStream => _messageReceivedController.stream; + + final _dtmfReceivedController = + StreamController<({JCCallItem item, int value})>.broadcast(); + Stream<({JCCallItem item, int value})> get dtmfReceivedStream => + _dtmfReceivedController.stream; + + final _earlyMediaController = StreamController.broadcast(); + Stream get earlyMediaStream => _earlyMediaController.stream; + + VideocallItem? _currentItem; + VideocallItem? get currentItem => _currentItem; + + // -- Lifecycle -- + + Future initialize() async { + final client = _clientRef.client; + final mediaDevice = _deviceRef.mediaDevice; + if (client == null || mediaDevice == null) return false; + _call = await JCCall.create(client, mediaDevice, this); + return _call != null; + } + + Future destroy() async => JCCall.destroy(); + + // -- Call actions -- + + Future startCall({ + required String userId, + required bool isVideo, + CallParam? callParam, + }) async { + if (_call == null) return false; + return _call!.call(userId, isVideo, callParam); + } + + Future answerCall({required bool isVideo}) async { + final item = await _call?.getActiveCallItem(); + if (item == null) return false; + return _call!.answer(item, isVideo); + } + + Future hangUp({int? reason, String? description}) async { + final item = await _call?.getActiveCallItem(); + if (item == null) return false; + return _call!.term(item, reason ?? JCCall.REASON_NONE, description ?? ''); + } + + Future termCall( + JCCallItem item, int reason, String description) async { + if (_call == null) return false; + return _call!.term(item, reason, description); + } + + // -- Mute/Audio -- + + Future mute(JCCallItem item) async { + if (_call == null) return false; + return _call!.mute(item); + } + + Future muteMicrophone(JCCallItem item, bool mute) async { + if (_call == null) return false; + return _call!.muteMicrophone(item, mute); + } + + Future muteSpeaker(JCCallItem item, bool mute) async { + if (_call == null) return false; + return _call!.muteSpeaker(item, mute); + } + + Future setMicScale(JCCallItem item, int scale) async { + if (_call == null) return false; + return _call!.setMicScale(item, scale); + } + + // -- Hold -- + + Future hold(JCCallItem item) async { + if (_call == null) return false; + return _call!.hold(item); + } + + Future becomeActive(JCCallItem item) async { + if (_call == null) return false; + return _call!.becomeActive(item); + } + + // -- Video -- + + Future enableUploadVideoStream(JCCallItem item) async { + if (_call == null) return false; + return _call!.enableUploadVideoStream(item); + } + + Future startLocalVideo( + {int renderType = JCMediaDevice.RENDER_FULL_AUTO}) async { + final item = await _call?.getActiveCallItem(); + if (item == null) return null; + return item.startSelfVideo(renderType); + } + + Future stopLocalVideo() async { + final item = await _call?.getActiveCallItem(); + if (item == null) return false; + return item.stopSelfVideo(); + } + + Future startRemoteVideo( + {int renderType = JCMediaDevice.RENDER_FULL_CONTENT}) async { + final item = await _call?.getActiveCallItem(); + if (item == null) return null; + return item.startOtherVideo(renderType); + } + + Future stopRemoteVideo() async { + final item = await _call?.getActiveCallItem(); + if (item == null) return false; + return item.stopOtherVideo(); + } + + // -- Recording -- + + Future audioRecord( + JCCallItem item, bool enable, String filePath) async { + if (_call == null) return false; + return _call!.audioRecord(item, enable, filePath); + } + + Future videoRecord( + JCCallItem item, { + required bool enable, + required bool remote, + required int width, + required int height, + required String filePath, + required bool bothAudio, + required int keyframe, + }) async { + if (_call == null) return false; + return _call!.videoRecord( + item, enable, remote, width, height, filePath, bothAudio, keyframe); + } + + // -- Messaging -- + + Future sendMessage( + JCCallItem item, String type, String content) async { + if (_call == null) return false; + return _call!.sendMessage(item, type, content); + } + + Future sendDtmf(JCCallItem item, int value) async { + if (_call == null) return false; + return _call!.sendDtmf(item, value); + } + + // -- Items -- + + Future?> getCallItems() async => _call?.getCallItems(); + + Future getActiveCallItem() async => + _call?.getActiveCallItem(); + + // -- Config -- + + Future getStatistics() async { + if (_call == null) return ''; + return _call!.getStatistics(); + } + + Future updateMediaConfig(MediaConfig mediaConfig) async { + if (_call == null) return false; + return _call!.updateMediaConfig(mediaConfig); + } + + Future getMediaConfig() async => _call?.getMediaConfig(); + + Future setMaxCallNum(int num) async { + if (_call == null) return false; + return _call!.setMaxCallNum(num); + } + + Future setTermWhenNetDisconnected(bool term) async { + if (_call == null) return false; + return _call!.setTermWhenNetDisconnected(term); + } + + static Future generateMediaConfigByMode(int mode) => + JCCall.generateByMode(mode); + + // -- Dispose -- + + void dispose() { + _callItemAddController.close(); + _callItemUpdateController.close(); + _callItemRemoveController.close(); + _missedCallController.close(); + _messageReceivedController.close(); + _dtmfReceivedController.close(); + _earlyMediaController.close(); + } + + // -- Internal -- + + VideocallState _mapCallState(int state) { + if (state == JCCall.STATE_PENDING) return VideocallState.pending; + if (state == JCCall.STATE_CONNECTING) return VideocallState.connecting; + if (state == JCCall.STATE_TALKING) return VideocallState.talking; + if (state == JCCall.STATE_OK) return VideocallState.ok; + if (state == JCCall.STATE_CANCEL) return VideocallState.canceled; + if (state == JCCall.STATE_CANCELED) return VideocallState.canceled; + if (state == JCCall.STATE_MISSED) return VideocallState.missed; + return VideocallState.idle; + } + + VideocallItem _buildItem(JCCallItem item, VideocallState state) { + return VideocallItem( + userId: item.getUserId(), + isVideo: item.getVideo(), + direction: item.getDirection() == JCCall.DIRECTION_IN + ? CallDirection.incoming + : CallDirection.outgoing, + state: state, + ); + } + + // -- JCCallCallback -- + + @override + void onCallItemAdd(JCCallItem item) { + _currentItem = _buildItem(item, VideocallState.pending); + _callItemAddController.add(_currentItem!); + } + + @override + void onCallItemUpdate(JCCallItem item, ChangeParam changeParam) { + _currentItem = _buildItem(item, _mapCallState(item.getState())); + _callItemUpdateController.add(_currentItem!); + } + + @override + void onCallItemRemove(JCCallItem item, int reason, String description) { + _currentItem = null; + _callItemRemoveController.add((reason: reason, description: description)); + } + + @override + void onMessageReceive(String type, String content, JCCallItem item) { + _messageReceivedController.add((type: type, content: content, item: item)); + } + + @override + void onMissedCallItem(JCCallItem item) { + _missedCallController.add(_buildItem(item, VideocallState.missed)); + } + + @override + void onDtmfReceived(JCCallItem item, int value) { + _dtmfReceivedController.add((item: item, value: value)); + } + + @override + void onEarlyMediaReceived(JCCallItem item) { + _earlyMediaController.add(item); + } + + @override + void onSipRingInfoReceived(JCCallItem item, String callSipType) {} +} diff --git a/packages/videocall_sdk/lib/src/services/videocall_channel_service.dart b/packages/videocall_sdk/lib/src/services/videocall_channel_service.dart new file mode 100644 index 00000000..2a740ca0 --- /dev/null +++ b/packages/videocall_sdk/lib/src/services/videocall_channel_service.dart @@ -0,0 +1,399 @@ +import 'dart:async'; + +import 'package:jc_sdk/jc_sdk.dart'; + +import 'videocall_client.dart'; +import 'videocall_device_service.dart'; + +class VideocallChannelService with JCMediaChannelCallback { + VideocallChannelService({ + required VideocallClient client, + required VideocallDeviceService deviceService, + }) : _clientRef = client, + _deviceRef = deviceService; + + final VideocallClient _clientRef; + final VideocallDeviceService _deviceRef; + JCMediaChannel? _channel; + JCMediaChannel? get channel => _channel; + + // -- Streams -- + + final _stateChangeController = + StreamController<({int state, int oldState})>.broadcast(); + Stream<({int state, int oldState})> get stateChangeStream => + _stateChangeController.stream; + + final _joinController = + StreamController<({bool result, int reason, String channelId})> + .broadcast(); + Stream<({bool result, int reason, String channelId})> get joinStream => + _joinController.stream; + + final _leaveController = + StreamController<({int reason, String channelId})>.broadcast(); + Stream<({int reason, String channelId})> get leaveStream => + _leaveController.stream; + + final _stopController = + StreamController<({bool result, int reason})>.broadcast(); + Stream<({bool result, int reason})> get stopStream => _stopController.stream; + + final _participantJoinController = + StreamController.broadcast(); + Stream get participantJoinStream => + _participantJoinController.stream; + + final _participantLeftController = + StreamController.broadcast(); + Stream get participantLeftStream => + _participantLeftController.stream; + + final _participantUpdateController = StreamController< + ({ + JCMediaChannelParticipant participant, + ChannelChangeParam changeParam, + })>.broadcast(); + Stream< + ({ + JCMediaChannelParticipant participant, + ChannelChangeParam changeParam, + })> get participantUpdateStream => _participantUpdateController.stream; + + final _participantVolumeChangeController = + StreamController.broadcast(); + Stream get participantVolumeChangeStream => + _participantVolumeChangeController.stream; + + final _queryController = StreamController< + ({ + int operationId, + bool result, + int reason, + JCMediaChannelQueryInfo queryInfo, + })>.broadcast(); + Stream< + ({ + int operationId, + bool result, + int reason, + JCMediaChannelQueryInfo queryInfo, + })> get queryStream => _queryController.stream; + + final _propertyChangeController = + StreamController.broadcast(); + Stream get propertyChangeStream => + _propertyChangeController.stream; + + final _messageReceivedController = + StreamController<({String type, String content, String fromUserId})> + .broadcast(); + Stream<({String type, String content, String fromUserId})> + get messageReceivedStream => _messageReceivedController.stream; + + final _inviteSipUserResultController = + StreamController<({int operationId, bool result, int reason})> + .broadcast(); + Stream<({int operationId, bool result, int reason})> + get inviteSipUserResultStream => _inviteSipUserResultController.stream; + + // -- Lifecycle -- + + Future initialize() async { + final client = _clientRef.client; + final mediaDevice = _deviceRef.mediaDevice; + if (client == null || mediaDevice == null) return false; + _channel = await JCMediaChannel.create(client, mediaDevice, this); + return _channel != null; + } + + Future destroy() async => JCMediaChannel.destroy(); + + // -- Channel actions -- + + Future join(String channelId, {JoinParam? joinParam}) async { + if (_channel == null) return false; + return _channel!.join(channelId, joinParam); + } + + Future leave() async { + if (_channel == null) return false; + return _channel!.leave(); + } + + Future stop() async { + if (_channel == null) return false; + return _channel!.stop(); + } + + Future query(String channelId) async { + if (_channel == null) return -1; + return _channel!.query(channelId); + } + + // -- Audio/Video streams -- + + Future enableUploadAudioStream(bool enable) async { + if (_channel == null) return false; + return _channel!.enableUploadAudioStream(enable); + } + + Future enableUploadVideoStream(bool enable) async { + if (_channel == null) return false; + return _channel!.enableUploadVideoStream(enable); + } + + Future enableAudioOutput(bool enable) async { + if (_channel == null) return false; + return _channel!.enableAudioOutput(enable); + } + + Future requestVideo( + JCMediaChannelParticipant participant, int pictureSize) async { + if (_channel == null) return false; + return _channel!.requestVideo(participant, pictureSize); + } + + Future requestScreenVideo(String screenUri, int pictureSize) async { + if (_channel == null) return false; + return _channel!.requestScreenVideo(screenUri, pictureSize); + } + + // -- Screen share / CDN / Recording -- + + Future enableScreenShare( + bool enable, ScreenShareParam? screenShareParam) async { + if (_channel == null) return false; + return _channel!.enableScreenShare(enable, screenShareParam); + } + + Future enableCdn(bool enable, int keyInterval) async { + if (_channel == null) return false; + return _channel!.enableCdn(enable, keyInterval); + } + + Future enableRecord(bool enable, {RecordParam? recordParam}) async { + if (_channel == null) return false; + return _channel!.enableRecord(enable, recordParam); + } + + // -- Participants -- + + Future?> getParticipants() async => + _channel?.getParticipants(); + + Future getParticipant(String userId) async => + _channel?.getParticipant(userId); + + Future kick(JCMediaChannelParticipant participant) async { + if (_channel == null) return false; + return _channel!.kick(participant); + } + + Future inviteSipUser(String userId, {SipParam? sipParam}) async { + if (_channel == null) return -1; + return _channel!.inviteSipUser(userId, sipParam); + } + + // -- Custom roles/state -- + + Future setCustomRole(int customRole, + {JCMediaChannelParticipant? participant}) async { + if (_channel == null) return false; + return _channel!.setCustomRole(customRole, participant); + } + + Future getCustomRole() async { + if (_channel == null) return 0; + return _channel!.getCustomRole(); + } + + Future setCustomState(int customState, + {JCMediaChannelParticipant? participant}) async { + if (_channel == null) return false; + return _channel!.setCustomState(customState, participant); + } + + Future getCustomState() async { + if (_channel == null) return 0; + return _channel!.getCustomState(); + } + + // -- Channel properties -- + + Future setCustomProperty(String property) async { + if (_channel == null) return -1; + return _channel!.setCustomProperty(property); + } + + Future getCustomProperty() async { + if (_channel == null) return ''; + return _channel!.getCustomProperty(); + } + + // -- Channel info -- + + Future getChannelUri() async { + if (_channel == null) return ''; + return _channel!.getChannelUri(); + } + + Future getChannelId() async { + if (_channel == null) return ''; + return _channel!.getChannelId(); + } + + Future getSessionId() async { + if (_channel == null) return ''; + return _channel!.getSessionId(); + } + + Future getConfId() async { + if (_channel == null) return -1; + return _channel!.getConfId(); + } + + Future getPassword() async { + if (_channel == null) return ''; + return _channel!.getPassword(); + } + + Future getChannelNumber() async { + if (_channel == null) return 0; + return _channel!.getChannelNumber(); + } + + Future getTitle() async { + if (_channel == null) return ''; + return _channel!.getTitle(); + } + + Future getChannelState() async { + if (_channel == null) return JCMediaChannel.STATE_IDLE; + return _channel!.getState(); + } + + Future getUploadLocalAudio() async { + if (_channel == null) return false; + return _channel!.getUploadLocalAudio(); + } + + Future getUploadLocalVideo() async { + if (_channel == null) return false; + return _channel!.getUploadLocalVideo(); + } + + Future getAudioOutput() async { + if (_channel == null) return false; + return _channel!.getAudioOutput(); + } + + Future getRecordState() async { + if (_channel == null) return JCMediaChannel.RECORD_STATE_NONE; + return _channel!.getRecordState(); + } + + Future getCdnState() async { + if (_channel == null) return JCMediaChannel.CDN_STATE_NONE; + return _channel!.getCdnState(); + } + + // -- Messaging -- + + Future sendMessage( + String type, String content, String toUserId) async { + if (_channel == null) return false; + return _channel!.sendMessage(type, content, toUserId); + } + + Future sendCommand(String name, String param) async { + if (_channel == null) return false; + return _channel!.sendCommand(name, param); + } + + // -- Statistics -- + + Future getStatistics() async { + if (_channel == null) return ''; + return _channel!.getStatistics(); + } + + // -- Dispose -- + + void dispose() { + _stateChangeController.close(); + _joinController.close(); + _leaveController.close(); + _stopController.close(); + _participantJoinController.close(); + _participantLeftController.close(); + _participantUpdateController.close(); + _participantVolumeChangeController.close(); + _queryController.close(); + _propertyChangeController.close(); + _messageReceivedController.close(); + _inviteSipUserResultController.close(); + } + + // -- JCMediaChannelCallback -- + + @override + void onMediaChannelStateChange(int state, int oldState) => + _stateChangeController.add((state: state, oldState: oldState)); + + @override + void onMediaChannelPropertyChange(PropChangeParam propChangeParam) => + _propertyChangeController.add(propChangeParam); + + @override + void onJoin(bool result, int reason, String channelId) => + _joinController + .add((result: result, reason: reason, channelId: channelId)); + + @override + void onLeave(int reason, String channelId) => + _leaveController.add((reason: reason, channelId: channelId)); + + @override + void onStop(bool result, int reason) => + _stopController.add((result: result, reason: reason)); + + @override + void onQuery(int operationId, bool result, int reason, + JCMediaChannelQueryInfo queryInfo) => + _queryController.add(( + operationId: operationId, + result: result, + reason: reason, + queryInfo: queryInfo, + )); + + @override + void onParticipantJoin(JCMediaChannelParticipant participant) => + _participantJoinController.add(participant); + + @override + void onParticipantLeft(JCMediaChannelParticipant participant) => + _participantLeftController.add(participant); + + @override + void onParticipantUpdate(JCMediaChannelParticipant participant, + ChannelChangeParam changeParam) => + _participantUpdateController + .add((participant: participant, changeParam: changeParam)); + + @override + void onParticipantVolumeChange(JCMediaChannelParticipant participant) => + _participantVolumeChangeController.add(participant); + + @override + void onChannelMessageReceive( + String type, String content, String fromUserId) => + _messageReceivedController + .add((type: type, content: content, fromUserId: fromUserId)); + + @override + void onInviteSipUserResult(int operationId, bool result, int reason) => + _inviteSipUserResultController + .add((operationId: operationId, result: result, reason: reason)); +} diff --git a/packages/videocall_sdk/lib/src/services/videocall_client.dart b/packages/videocall_sdk/lib/src/services/videocall_client.dart new file mode 100644 index 00000000..3d87f03f --- /dev/null +++ b/packages/videocall_sdk/lib/src/services/videocall_client.dart @@ -0,0 +1,247 @@ +import 'dart:async'; + +import 'package:jc_sdk/jc_sdk.dart'; + +import '../config/videocall_sdk_config.dart'; +import '../models/login_failure_reason.dart'; +import '../models/videocall_client_state.dart'; + +class VideocallClient with JCClientCallback { + VideocallClient(this._config); + + final VideocallSdkConfig _config; + JCClient? _client; + JCClient? get client => _client; + + // -- Streams -- + + final _stateController = StreamController.broadcast(); + Stream get stateStream => _stateController.stream; + + final _loginResultController = + StreamController<({bool success, LoginFailureReason? reason})>.broadcast(); + Stream<({bool success, LoginFailureReason? reason})> get loginResultStream => + _loginResultController.stream; + + final _logoutController = StreamController.broadcast(); + Stream get logoutStream => _logoutController.stream; + + final _onlineMessageReceivedController = + StreamController<({String userId, String content})>.broadcast(); + Stream<({String userId, String content})> get onlineMessageReceivedStream => + _onlineMessageReceivedController.stream; + + final _onlineMessageSendResultController = + StreamController<({int operationId, bool result})>.broadcast(); + Stream<({int operationId, bool result})> + get onlineMessageSendResultStream => + _onlineMessageSendResultController.stream; + + final _serverMessageController = + StreamController<({String type, String params, String message})> + .broadcast(); + Stream<({String type, String params, String message})> + get serverMessageStream => _serverMessageController.stream; + + VideocallClientState _state = VideocallClientState.notInitialized; + VideocallClientState get state => _state; + + // -- Lifecycle -- + + Future initialize() async { + try { + _client = await JCClient.create(_config.appKey, this, _config.createParam); + } catch (_) { + return false; + } + if (_config.serverAddress.isNotEmpty) { + await _client!.setServerAddress(_config.serverAddress); + } + final clientState = await _client!.getState(); + _updateState( + clientState >= JCClient.STATE_IDLE + ? VideocallClientState.idle + : VideocallClientState.notInitialized, + ); + return _state == VideocallClientState.idle; + } + + Future destroy() async { + final result = await JCClient.destroy(); + if (result) { + _client = null; + _updateState(VideocallClientState.notInitialized); + } + return result; + } + + // -- Auth -- + + Future login({ + required String userId, + required String password, + LoginParam? loginParam, + }) async { + if (_client == null || _state != VideocallClientState.idle) return false; + return _client!.login(userId, password, loginParam ?? LoginParam()); + } + + Future relogin({ + required String userId, + required String password, + LoginParam? loginParam, + }) async { + if (_client == null) return false; + return _client!.relogin(userId, password, loginParam ?? LoginParam()); + } + + Future logout() async { + if (_client == null) return false; + return _client!.logout(); + } + + // -- User info -- + + Future getUserId() async => _client?.getUserId(); + + Future getDisplayName() async => _client?.getDisplayName(); + + Future setDisplayName(String displayName) async { + if (_client == null) return false; + return _client!.setDisplayName(displayName); + } + + Future getAppKey() async => _client?.getAppkey(); + + Future getServerUid() async { + if (_client == null) return ''; + return _client!.getServerUid(); + } + + // -- Server config -- + + Future setServerAddress(String serverAddress) async { + if (_client == null) return false; + return _client!.setServerAddress(serverAddress); + } + + Future getServerAddress() async { + if (_client == null) return ''; + return _client!.getServerAddress(); + } + + // -- Foreground/background -- + + Future setForeground(bool foreground) async { + if (_client == null) return false; + return _client!.setForeground(foreground); + } + + // -- Online messaging -- + + Future sendOnlineMessage({ + required String userId, + required String content, + }) async { + if (_client == null) return -1; + return _client!.sendOnlineMessage(userId, content); + } + + // -- Params -- + + Future getCreateParam() async => _client?.getCreateParam(); + + Future getLoginParam() async => _client?.getLoginParam(); + + Future getState() async { + if (_client == null) return JCClient.STATE_NOT_INIT; + return _client!.getState(); + } + + // -- Dispose -- + + void dispose() { + _stateController.close(); + _loginResultController.close(); + _logoutController.close(); + _onlineMessageReceivedController.close(); + _onlineMessageSendResultController.close(); + _serverMessageController.close(); + } + + // -- Internal -- + + void _updateState(VideocallClientState newState) { + _state = newState; + _stateController.add(newState); + } + + VideocallClientState _mapState(int state) { + if (state == JCClient.STATE_IDLE) return VideocallClientState.idle; + if (state == JCClient.STATE_LOGINING) return VideocallClientState.loggingIn; + if (state == JCClient.STATE_LOGINED) return VideocallClientState.loggedIn; + if (state == JCClient.STATE_LOGOUTING) { + return VideocallClientState.loggingOut; + } + return VideocallClientState.notInitialized; + } + + LoginFailureReason _mapLoginReason(int reason) { + if (reason == JCClient.REASON_AUTH) return LoginFailureReason.auth; + if (reason == JCClient.REASON_NETWORK) return LoginFailureReason.network; + if (reason == JCClient.REASON_APPKEY) return LoginFailureReason.appkey; + if (reason == JCClient.REASON_NOUSER) return LoginFailureReason.noUser; + if (reason == JCClient.REASON_TIMEOUT) return LoginFailureReason.timeout; + if (reason == JCClient.REASON_SERVER_LOGOUT) { + return LoginFailureReason.serverLogout; + } + if (reason == JCClient.REASON_ANOTHER_DEVICE_LOGINED) { + return LoginFailureReason.anotherDeviceLoggedIn; + } + if (reason == JCClient.REASON_TOKEN_MISMATCH) { + return LoginFailureReason.tokenMismatch; + } + if (reason == JCClient.REASON_TOKEN_EXPIRED) { + return LoginFailureReason.tokenExpired; + } + return LoginFailureReason.unknown; + } + + // -- JCClientCallback -- + + @override + void onClientStateChange(int state, int oldState) { + _updateState(_mapState(state)); + } + + @override + void onLogin(bool result, int reason) { + _loginResultController.add(( + success: result, + reason: result ? null : _mapLoginReason(reason), + )); + } + + @override + void onLogout(int reason) { + _logoutController.add(reason); + _updateState(VideocallClientState.idle); + } + + @override + void onOnlineMessageReceive(String userId, String content) { + _onlineMessageReceivedController.add((userId: userId, content: content)); + } + + @override + void onOnlineMessageSendResult(int operationId, bool result) { + _onlineMessageSendResultController + .add((operationId: operationId, result: result)); + } + + @override + void onServerMessageReceive(String type, String params, String message) { + _serverMessageController + .add((type: type, params: params, message: message)); + } +} diff --git a/packages/videocall_sdk/lib/src/services/videocall_device_service.dart b/packages/videocall_sdk/lib/src/services/videocall_device_service.dart new file mode 100644 index 00000000..b23c3997 --- /dev/null +++ b/packages/videocall_sdk/lib/src/services/videocall_device_service.dart @@ -0,0 +1,394 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:jc_sdk/jc_sdk.dart'; + +import 'videocall_client.dart'; + +class VideocallDeviceService with JCMediaDeviceCallback { + VideocallDeviceService({required VideocallClient client}) : _clientRef = client; + + final VideocallClient _clientRef; + JCMediaDevice? _mediaDevice; + JCMediaDevice? get mediaDevice => _mediaDevice; + + // -- Streams -- + + final _cameraUpdateController = StreamController.broadcast(); + Stream get cameraUpdateStream => _cameraUpdateController.stream; + + final _audioOutputTypeController = StreamController.broadcast(); + Stream get audioOutputTypeStream => _audioOutputTypeController.stream; + + final _renderReceivedController = + StreamController.broadcast(); + Stream get renderReceivedStream => + _renderReceivedController.stream; + + final _renderStartController = + StreamController.broadcast(); + Stream get renderStartStream => + _renderStartController.stream; + + final _videoErrorController = + StreamController.broadcast(); + Stream get videoErrorStream => + _videoErrorController.stream; + + final _audioErrorController = StreamController.broadcast(); + Stream get audioErrorStream => _audioErrorController.stream; + + final _audioResumeController = StreamController.broadcast(); + Stream get audioResumeStream => _audioResumeController.stream; + + // -- Lifecycle -- + + Future initialize() async { + final client = _clientRef.client; + if (client == null) return false; + _mediaDevice = await JCMediaDevice.create(client, this); + return _mediaDevice != null; + } + + Future destroy() async => JCMediaDevice.destroy(); + + // -- Camera -- + + Future isCameraOpen() async { + if (_mediaDevice == null) return false; + return _mediaDevice!.isCameraOpen(); + } + + Future getCamera() async => _mediaDevice?.getCamera(); + + Future?> getCameras() async => + _mediaDevice?.getCameras(); + + Future?> getExtCameras() async => + _mediaDevice?.getExtCameras(); + + Future getDefaultCamera() async => + _mediaDevice?.getDefaultCamera(); + + Future setDefaultCamera(String cameraId) async { + if (_mediaDevice == null) return false; + return _mediaDevice!.setDefaultCamera(cameraId); + } + + Future startCamera() async { + if (_mediaDevice == null) return false; + return _mediaDevice!.startCamera(); + } + + Future stopCamera() async { + if (_mediaDevice == null) return false; + return _mediaDevice!.stopCamera(); + } + + Future switchCamera({JCMediaDeviceCamera? camera}) async { + if (_mediaDevice == null) return false; + return _mediaDevice!.switchCamera(camera: camera); + } + + Future setCameraProperty(int width, int height, int frameRate) async { + if (_mediaDevice == null) return false; + return _mediaDevice!.setCameraProperty(width, height, frameRate); + } + + Future setScreenCaptureProperty( + int width, int height, int frameRate) async { + if (_mediaDevice == null) return false; + return _mediaDevice!.setScreenCaptureProperty(width, height, frameRate); + } + + Future getCameraType(int cameraIndex) async { + if (_mediaDevice == null) return JCMediaDevice.CAMERA_NONE; + return _mediaDevice!.getCameraType(cameraIndex); + } + + // -- Exposure -- + + Future getMinExposureCompensation() async { + if (_mediaDevice == null) return 0; + return _mediaDevice!.getMinExposureCompensation(); + } + + Future getMaxExposureCompensation() async { + if (_mediaDevice == null) return 0; + return _mediaDevice!.getMaxExposureCompensation(); + } + + Future getExposureCompensationStep() async { + if (_mediaDevice == null) return 0; + return _mediaDevice!.getExposureCompensationStep(); + } + + Future setIOSExposureCompensation(double level) async { + if (_mediaDevice == null) return false; + return _mediaDevice!.setIOSExposureCompensation(level); + } + + Future setExposureCompensation(int level) async { + if (_mediaDevice == null) return false; + return _mediaDevice!.setExposureCompensation(level); + } + + // -- Flash -- + + Future isCameraFlashSupported() async { + if (_mediaDevice == null) return false; + return _mediaDevice!.isCameraFlashSupported(); + } + + Future enableFlash(bool enable) async { + if (_mediaDevice == null) return false; + return _mediaDevice!.enableFlash(enable); + } + + // -- Focus/Zoom -- + + Future handleFocusMetering( + JCMediaDeviceVideoCanvas canvas, double xPercent, double yPercent) async { + if (_mediaDevice == null) return false; + return _mediaDevice!.handleFocusMetering(canvas, xPercent, yPercent); + } + + Future setCameraZoomRatio(double zoomRatio) async { + if (_mediaDevice == null) return false; + return _mediaDevice!.setCameraZoomRatio(zoomRatio); + } + + Future getCameraMaxZoom() async { + if (_mediaDevice == null) return 1.0; + return _mediaDevice!.getCameraMaxZoom(); + } + + Future getCameraCurrentZoom() async { + if (_mediaDevice == null) return 0; + return _mediaDevice!.getCameraCurrentZoom(); + } + + // -- Speaker -- + + Future isSpeakerOn() async { + if (_mediaDevice == null) return false; + return _mediaDevice!.isSpeakerOn(); + } + + Future enableSpeaker(bool enable) async { + if (_mediaDevice == null) return false; + return _mediaDevice!.enableSpeaker(enable); + } + + Future getDefaultSpeakerOn() async { + if (_mediaDevice == null) return false; + return _mediaDevice!.getDefaultSpeakerOn(); + } + + Future setDefaultSpeakerOn(bool state) async { + if (_mediaDevice == null) return false; + return _mediaDevice!.setDefaultSpeakerOn(state); + } + + Future getAudioRouteType() async { + if (_mediaDevice == null) return 0; + return _mediaDevice!.getAudioRouteType(); + } + + // -- Audio -- + + Future isAudioStart() async { + if (_mediaDevice == null) return false; + return _mediaDevice!.isAudioStart(); + } + + Future startAudio() async { + if (_mediaDevice == null) return false; + return _mediaDevice!.startAudio(); + } + + Future stopAudio() async { + if (_mediaDevice == null) return false; + return _mediaDevice!.stopAudio(); + } + + Future setAudioAecMode(int mode) async { + if (_mediaDevice == null) return false; + return _mediaDevice!.setAudioAecMode(mode); + } + + Future autoStartAudioOutput(bool enable) async { + if (_mediaDevice == null) return false; + return _mediaDevice!.autoStartAudioOutput(enable); + } + + Future autoStartAudioInput(bool enable) async { + if (_mediaDevice == null) return false; + return _mediaDevice!.autoStartAudioInput(enable); + } + + Future setDeviceAudioAutoInput(bool auto) async { + if (_mediaDevice == null) return false; + return _mediaDevice!.setDeviceAudioAutoInput(auto); + } + + Future setDeviceAudioAutoOutput(bool auto) async { + if (_mediaDevice == null) return false; + return _mediaDevice!.setDeviceAudioAutoOutput(auto); + } + + Future getUseInternalAudioDeviceLogic() async { + if (_mediaDevice == null) return false; + return _mediaDevice!.getUseInternalAudioDeviceLogic(); + } + + Future getAudioParam() async => + _mediaDevice?.getAudioParam(); + + // -- Volume -- + + Future getOutputVolume() async { + if (_mediaDevice == null) return 0; + return _mediaDevice!.getOutputVolume(); + } + + Future getInputVolume() async { + if (_mediaDevice == null) return 0; + return _mediaDevice!.getInputVolume(); + } + + bool addVolumeCallback(JCMediaVolumeCallback callback) { + if (_mediaDevice == null) return false; + return _mediaDevice!.addVolumeCallback(callback); + } + + bool removeVolumeCallback(JCMediaVolumeCallback callback) { + if (_mediaDevice == null) return false; + return _mediaDevice!.removeVolumeCallback(callback); + } + + // -- Video rendering -- + + Future startCameraVideo( + {int renderType = JCMediaDevice.RENDER_FULL_AUTO}) async { + return _mediaDevice?.startCameraVideo(renderType); + } + + Future startVideo( + String? videoSource, int renderType) async { + return _mediaDevice?.startVideo(videoSource, renderType); + } + + Future stopVideo(JCMediaDeviceVideoCanvas canvas) async { + if (_mediaDevice == null) return false; + return _mediaDevice!.stopVideo(canvas); + } + + // -- Video file (custom capture) -- + + Future isVideoFileOpen() async { + if (_mediaDevice == null) return false; + return _mediaDevice!.isVideoFileOpen(); + } + + Future getVideoFileId() async { + if (_mediaDevice == null) return ''; + return _mediaDevice!.getVideoFileId(); + } + + Future startVideoFile() async { + if (_mediaDevice == null) return false; + return _mediaDevice!.startVideoFile(); + } + + Future setVideoFileFrame(Uint8List data, int format, int width, + int height, int angle, int mirror, bool keyFrame) async { + if (_mediaDevice == null) return false; + return _mediaDevice! + .setVideoFileFrame(data, format, width, height, angle, mirror, keyFrame); + } + + Future stopVideoFile() async { + if (_mediaDevice == null) return false; + return _mediaDevice!.stopVideoFile(); + } + + // -- Video angle -- + + Future setVideoAngle(int angle) async { + if (_mediaDevice == null) return false; + return _mediaDevice!.setVideoAngle(angle); + } + + Future getVideoAngle() async { + if (_mediaDevice == null) return 0; + return _mediaDevice!.getVideoAngle(); + } + + // -- Frame callbacks -- + + Future setAudioFrameCallback(JCAudioFrameCallback? callback) async { + if (_mediaDevice == null) return false; + return _mediaDevice!.setAudioFrameCallback(callback); + } + + Future setVideoFrameCallback(JCVideoFrameCallback? callback) async { + if (_mediaDevice == null) return false; + return _mediaDevice!.setVideoFrameCallback(callback); + } + + // -- Custom audio -- + + Future inputCustomAudioData(int sampleRateHz, int channels, + Uint8List byteBuffer, int playDelayMS, int recDelayMS, int clockDrift) async { + if (_mediaDevice == null) return false; + return _mediaDevice!.inputCustomAudioData( + sampleRateHz, channels, byteBuffer, playDelayMS, recDelayMS, clockDrift); + } + + Future getAudioOutputData(int sampleRateHz, int channels) async { + return _mediaDevice?.getAudioOutputData(sampleRateHz, channels); + } + + // -- Dispose -- + + void dispose() { + _cameraUpdateController.close(); + _audioOutputTypeController.close(); + _renderReceivedController.close(); + _renderStartController.close(); + _videoErrorController.close(); + _audioErrorController.close(); + _audioResumeController.close(); + } + + // -- JCMediaDeviceCallback -- + + @override + void onCameraUpdate() => _cameraUpdateController.add(null); + + @override + void onAudioOutputTypeChange(int audioRouteType) => + _audioOutputTypeController.add(audioRouteType); + + @override + void onRenderReceived(JCMediaDeviceVideoCanvas canvas) => + _renderReceivedController.add(canvas); + + @override + void onRenderStart(JCMediaDeviceVideoCanvas canvas) => + _renderStartController.add(canvas); + + @override + void onVideoError(JCMediaDeviceVideoCanvas canvas) => + _videoErrorController.add(canvas); + + @override + void onAudioError(bool background) => _audioErrorController.add(background); + + @override + void onAudioResume() => _audioResumeController.add(null); + + @override + void onNeedKeyFrame() {} +} diff --git a/packages/videocall_sdk/lib/src/services/videocall_log_service.dart b/packages/videocall_sdk/lib/src/services/videocall_log_service.dart new file mode 100644 index 00000000..a3ea7a4d --- /dev/null +++ b/packages/videocall_sdk/lib/src/services/videocall_log_service.dart @@ -0,0 +1,13 @@ +import 'package:jc_sdk/jc_sdk.dart'; + +class VideocallLogService { + VideocallLogService._(); + + static Future uploadLog(String reason) => JCLog.uploadLog(reason); + static Future info(String type, String format) => + JCLog.info(type, format); + static Future error(String type, String format) => + JCLog.error(type, format); + static Future debug(String type, String format) => + JCLog.debug(type, format); +} diff --git a/packages/videocall_sdk/lib/src/services/videocall_net_service.dart b/packages/videocall_sdk/lib/src/services/videocall_net_service.dart new file mode 100644 index 00000000..dfb80335 --- /dev/null +++ b/packages/videocall_sdk/lib/src/services/videocall_net_service.dart @@ -0,0 +1,42 @@ +import 'dart:async'; + +import 'package:jc_sdk/jc_sdk.dart'; + +class VideocallNetService with JCNetCallback { + // -- Streams -- + + final _netChangeController = + StreamController<({int newNetType, int oldNetType})>.broadcast(); + Stream<({int newNetType, int oldNetType})> get netChangeStream => + _netChangeController.stream; + + // -- Lifecycle -- + + void initialize() { + JCNet.getInstance().addCallback(this); + } + + void uninitialize() { + JCNet.getInstance().removeCallback(this); + } + + // -- Network info -- + + Future getNetType() async => JCNet.getInstance().getNetType(); + + Future hasNet() async => JCNet.getInstance().hasNet(); + + // -- Dispose -- + + void dispose() { + uninitialize(); + _netChangeController.close(); + } + + // -- JCNetCallback -- + + @override + void onNetChange(int newNetType, int oldNetType) { + _netChangeController.add((newNetType: newNetType, oldNetType: oldNetType)); + } +} diff --git a/packages/videocall_sdk/lib/src/services/videocall_push_service.dart b/packages/videocall_sdk/lib/src/services/videocall_push_service.dart new file mode 100644 index 00000000..d15a85db --- /dev/null +++ b/packages/videocall_sdk/lib/src/services/videocall_push_service.dart @@ -0,0 +1,45 @@ +import 'dart:async'; + +import 'package:jc_sdk/jc_sdk.dart'; + +import 'videocall_client.dart'; + +class VideocallPushService { + VideocallPushService({required VideocallClient client}) : _clientRef = client; + + final VideocallClient _clientRef; + JCPush? _push; + JCPush? get push => _push; + + // -- Lifecycle -- + + Future initialize() async { + final client = _clientRef.client; + if (client == null) return false; + _push = await JCPush.create(client); + return _push != null; + } + + Future destroy() async { + await JCPush.destroy(); + _push = null; + } + + // -- Push -- + + Future addPushInfo(JCPushTemplate info) async { + if (_push == null) return false; + return _push!.addPushInfo(info); + } + + Future addPushTemplate(String data) async { + if (_push == null) return false; + return _push!.addPushTemplate(data); + } + + // -- Dispose -- + + void dispose() { + _push = null; + } +} diff --git a/packages/videocall_sdk/lib/videocall_sdk.dart b/packages/videocall_sdk/lib/videocall_sdk.dart new file mode 100644 index 00000000..84ba1b82 --- /dev/null +++ b/packages/videocall_sdk/lib/videocall_sdk.dart @@ -0,0 +1,60 @@ +/// SaveFamily video call SDK wrapper around Juphoon jc_sdk. +library; + +// Config +export 'src/config/videocall_sdk_config.dart'; + +// Models +export 'src/models/call_direction.dart'; +export 'src/models/call_state.dart'; +export 'src/models/login_failure_reason.dart'; +export 'src/models/videocall_client_state.dart'; +export 'src/models/videocall_item.dart'; + +// Services +export 'src/services/videocall_client.dart'; +export 'src/services/videocall_call_service.dart'; +export 'src/services/videocall_device_service.dart'; +export 'src/services/videocall_channel_service.dart'; +export 'src/services/videocall_push_service.dart'; +export 'src/services/videocall_net_service.dart'; +export 'src/services/videocall_log_service.dart'; + +// Manager +export 'src/manager/videocall_sdk_manager.dart'; + +// DI +export 'src/di/videocall_sdk_module.dart'; + +// Providers +export 'src/providers/videocall_providers.dart'; + +// Re-export jc_sdk types needed by consumers +export 'package:jc_sdk/jc_sdk.dart' + show + CallParam, + ChangeParam, + CreateParam, + LoginParam, + MediaConfig, + JoinParam, + RecordParam, + ScreenShareParam, + SipParam, + PropChangeParam, + ChannelChangeParam, + JCCallItem, + JCMediaChannelParticipant, + JCMediaChannelQueryInfo, + JCMediaDeviceVideoCanvas, + JCMediaDeviceCamera, + JCMediaDeviceAudioParam, + RenderMirrorType, + JCPushTemplate, + JCAudioFrameCallback, + JCVideoFrameCallback, + JCMediaVolumeCallback, + JCClient, + JCCall, + JCMediaDevice, + JCMediaChannel; diff --git a/packages/videocall_sdk/pubspec.yaml b/packages/videocall_sdk/pubspec.yaml new file mode 100644 index 00000000..a4d30e18 --- /dev/null +++ b/packages/videocall_sdk/pubspec.yaml @@ -0,0 +1,21 @@ +name: videocall_sdk +description: Wrapper around Juphoon jc_sdk for video calling in SaveFamily. +version: 0.0.1 +resolution: workspace +homepage: + +environment: + sdk: ^3.9.2 + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + jc_sdk: ^2.16.5 + flutter_riverpod: ^3.0.3 + get_it: ^9.0.5 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^5.0.0 diff --git a/pubspec.lock b/pubspec.lock index b1d14bbb..1ddbf9bd 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1004,6 +1004,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" + jc_sdk: + dependency: transitive + description: + name: jc_sdk + sha256: "017148c51e6181870507d429b2e2b52df1b13168ebd9590b3a3fa86ab09274ba" + url: "https://pub.dev" + source: hosted + version: "2.16.5" js: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 69ed1442..f5098ce9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -36,6 +36,7 @@ workspace: - packages/sf_shared - packages/sf_tracking - packages/utils + - packages/videocall_sdk dependencies: flutter_secure_storage: ^9.2.4