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
This commit is contained in:
@@ -2,5 +2,6 @@
|
|||||||
"env": "development",
|
"env": "development",
|
||||||
"apiBaseUrl": "https://api-neki-b2b.neki.es/gateway/api/",
|
"apiBaseUrl": "https://api-neki-b2b.neki.es/gateway/api/",
|
||||||
"apiOrigin": "https://neki-b2b.neki.es",
|
"apiOrigin": "https://neki-b2b.neki.es",
|
||||||
"wsUrl": "wss://api-neki-b2b.neki.es/websocket"
|
"wsUrl": "wss://api-neki-b2b.neki.es/websocket",
|
||||||
|
"juphoonAppKey": "9efcf2d889dc8a0320925096"
|
||||||
}
|
}
|
||||||
@@ -2,5 +2,6 @@
|
|||||||
"env": "production",
|
"env": "production",
|
||||||
"apiBaseUrl": "https://api-platform.savefamily.app/gateway/api/",
|
"apiBaseUrl": "https://api-platform.savefamily.app/gateway/api/",
|
||||||
"apiOrigin": "https://platform.savefamily.app",
|
"apiOrigin": "https://platform.savefamily.app",
|
||||||
"wsUrl": "wss://api-platform.savefamily.app/websocket"
|
"wsUrl": "wss://api-platform.savefamily.app/websocket",
|
||||||
|
"juphoonAppKey": "9efcf2d889dc8a0320925096"
|
||||||
}
|
}
|
||||||
@@ -2,5 +2,6 @@
|
|||||||
"env": "staging",
|
"env": "staging",
|
||||||
"apiBaseUrl": "https://api-platform.pre.savefamilygps.net/gateway/api/",
|
"apiBaseUrl": "https://api-platform.pre.savefamilygps.net/gateway/api/",
|
||||||
"apiOrigin": "https://platform.pre.savefamilygps.net",
|
"apiOrigin": "https://platform.pre.savefamilygps.net",
|
||||||
"wsUrl": "wss://api-platform.pre.savefamilygps.net/websocket"
|
"wsUrl": "wss://api-platform.pre.savefamilygps.net/websocket",
|
||||||
|
"juphoonAppKey": "9efcf2d889dc8a0320925096"
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ abstract class Environment {
|
|||||||
static const apiBaseUrl = String.fromEnvironment('apiBaseUrl');
|
static const apiBaseUrl = String.fromEnvironment('apiBaseUrl');
|
||||||
static const apiOrigin = String.fromEnvironment('apiOrigin');
|
static const apiOrigin = String.fromEnvironment('apiOrigin');
|
||||||
static const wsUrl = String.fromEnvironment('wsUrl');
|
static const wsUrl = String.fromEnvironment('wsUrl');
|
||||||
|
static const juphoonAppKey = String.fromEnvironment('juphoonAppKey');
|
||||||
|
|
||||||
// --- Fase 2: Firebase & Sentry ---
|
// --- Fase 2: Firebase & Sentry ---
|
||||||
// static const sentryDsn = String.fromEnvironment('sentryDsn');
|
// static const sentryDsn = String.fromEnvironment('sentryDsn');
|
||||||
|
|||||||
14
apps/mobile_app/lib/config/env/save_family_videocall_config.dart
vendored
Normal file
14
apps/mobile_app/lib/config/env/save_family_videocall_config.dart
vendored
Normal file
@@ -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;
|
||||||
|
}
|
||||||
@@ -6,6 +6,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:intl/date_symbol_data_local.dart';
|
import 'package:intl/date_symbol_data_local.dart';
|
||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
import 'package:sca_treezor/sca_treezor.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/environment_enum.dart';
|
||||||
import 'package:sf_app_platform/config/env/save_family_env_config.dart';
|
import 'package:sf_app_platform/config/env/save_family_env_config.dart';
|
||||||
import 'package:sf_app_platform/core/config/app_mode.dart';
|
import 'package:sf_app_platform/core/config/app_mode.dart';
|
||||||
@@ -24,6 +26,7 @@ Future<void> initApp(EnvironmentEnum env) async {
|
|||||||
|
|
||||||
navigationModule();
|
navigationModule();
|
||||||
scaTreezorModule();
|
scaTreezorModule();
|
||||||
|
videocallSdkModule(SaveFamilyVideocallConfig());
|
||||||
themePackages();
|
themePackages();
|
||||||
|
|
||||||
await setupFirebase(env);
|
await setupFirebase(env);
|
||||||
|
|||||||
@@ -91,6 +91,8 @@ dependencies:
|
|||||||
path: ../../packages/sca_treezor
|
path: ../../packages/sca_treezor
|
||||||
payments:
|
payments:
|
||||||
path: ../../packages/payments
|
path: ../../packages/payments
|
||||||
|
videocall_sdk:
|
||||||
|
path: ../../packages/videocall_sdk
|
||||||
#dependencies go here
|
#dependencies go here
|
||||||
cupertino_icons: ^1.0.8
|
cupertino_icons: ^1.0.8
|
||||||
flutter_svg: ^2.2.2
|
flutter_svg: ^2.2.2
|
||||||
|
|||||||
31
packages/videocall_sdk/.gitignore
vendored
Normal file
31
packages/videocall_sdk/.gitignore
vendored
Normal file
@@ -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/
|
||||||
3
packages/videocall_sdk/CHANGELOG.md
Normal file
3
packages/videocall_sdk/CHANGELOG.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
## 0.0.1
|
||||||
|
|
||||||
|
* Initial release. Wrapper around Juphoon jc_sdk for video calling.
|
||||||
1
packages/videocall_sdk/LICENSE
Normal file
1
packages/videocall_sdk/LICENSE
Normal file
@@ -0,0 +1 @@
|
|||||||
|
TODO: Add your license here.
|
||||||
5
packages/videocall_sdk/README.md
Normal file
5
packages/videocall_sdk/README.md
Normal file
@@ -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.
|
||||||
4
packages/videocall_sdk/analysis_options.yaml
Normal file
4
packages/videocall_sdk/analysis_options.yaml
Normal file
@@ -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
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import 'package:jc_sdk/jc_sdk.dart';
|
||||||
|
|
||||||
|
abstract class VideocallSdkConfig {
|
||||||
|
String get appKey;
|
||||||
|
String get serverAddress;
|
||||||
|
CreateParam? get createParam;
|
||||||
|
}
|
||||||
65
packages/videocall_sdk/lib/src/di/videocall_sdk_module.dart
Normal file
65
packages/videocall_sdk/lib/src/di/videocall_sdk_module.dart
Normal file
@@ -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<VideocallSdkConfig>(config);
|
||||||
|
|
||||||
|
// Core client
|
||||||
|
getIt.registerLazySingleton<VideocallClient>(
|
||||||
|
() => VideocallClient(getIt<VideocallSdkConfig>()),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Device (depends on Client)
|
||||||
|
getIt.registerLazySingleton<VideocallDeviceService>(
|
||||||
|
() => VideocallDeviceService(client: getIt<VideocallClient>()),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Call (depends on Client + Device)
|
||||||
|
getIt.registerLazySingleton<VideocallCallService>(
|
||||||
|
() => VideocallCallService(
|
||||||
|
client: getIt<VideocallClient>(),
|
||||||
|
deviceService: getIt<VideocallDeviceService>(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Channel (depends on Client + Device)
|
||||||
|
getIt.registerLazySingleton<VideocallChannelService>(
|
||||||
|
() => VideocallChannelService(
|
||||||
|
client: getIt<VideocallClient>(),
|
||||||
|
deviceService: getIt<VideocallDeviceService>(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Push (depends on Client)
|
||||||
|
getIt.registerLazySingleton<VideocallPushService>(
|
||||||
|
() => VideocallPushService(client: getIt<VideocallClient>()),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Net (standalone)
|
||||||
|
getIt.registerLazySingleton<VideocallNetService>(
|
||||||
|
() => VideocallNetService(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Manager (orchestrator)
|
||||||
|
getIt.registerLazySingleton<VideocallSdkManager>(
|
||||||
|
() => VideocallSdkManager(
|
||||||
|
client: getIt<VideocallClient>(),
|
||||||
|
deviceService: getIt<VideocallDeviceService>(),
|
||||||
|
callService: getIt<VideocallCallService>(),
|
||||||
|
channelService: getIt<VideocallChannelService>(),
|
||||||
|
pushService: getIt<VideocallPushService>(),
|
||||||
|
netService: getIt<VideocallNetService>(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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<bool> 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<void> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
enum CallDirection {
|
||||||
|
incoming,
|
||||||
|
outgoing,
|
||||||
|
}
|
||||||
9
packages/videocall_sdk/lib/src/models/call_state.dart
Normal file
9
packages/videocall_sdk/lib/src/models/call_state.dart
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
enum VideocallState {
|
||||||
|
idle,
|
||||||
|
pending,
|
||||||
|
connecting,
|
||||||
|
talking,
|
||||||
|
ok,
|
||||||
|
canceled,
|
||||||
|
missed,
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
enum LoginFailureReason {
|
||||||
|
auth,
|
||||||
|
network,
|
||||||
|
appkey,
|
||||||
|
noUser,
|
||||||
|
timeout,
|
||||||
|
serverLogout,
|
||||||
|
anotherDeviceLoggedIn,
|
||||||
|
tokenMismatch,
|
||||||
|
tokenExpired,
|
||||||
|
unknown,
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
enum VideocallClientState {
|
||||||
|
notInitialized,
|
||||||
|
idle,
|
||||||
|
loggingIn,
|
||||||
|
loggedIn,
|
||||||
|
loggingOut,
|
||||||
|
}
|
||||||
30
packages/videocall_sdk/lib/src/models/videocall_item.dart
Normal file
30
packages/videocall_sdk/lib/src/models/videocall_item.dart
Normal file
@@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<VideocallSdkManager>((ref) {
|
||||||
|
return GetIt.I<VideocallSdkManager>();
|
||||||
|
});
|
||||||
|
|
||||||
|
final videocallClientProvider = Provider<VideocallClient>((ref) {
|
||||||
|
return GetIt.I<VideocallClient>();
|
||||||
|
});
|
||||||
|
|
||||||
|
final videocallCallServiceProvider = Provider<VideocallCallService>((ref) {
|
||||||
|
return GetIt.I<VideocallCallService>();
|
||||||
|
});
|
||||||
|
|
||||||
|
final videocallDeviceServiceProvider = Provider<VideocallDeviceService>((ref) {
|
||||||
|
return GetIt.I<VideocallDeviceService>();
|
||||||
|
});
|
||||||
|
|
||||||
|
final videocallChannelServiceProvider =
|
||||||
|
Provider<VideocallChannelService>((ref) {
|
||||||
|
return GetIt.I<VideocallChannelService>();
|
||||||
|
});
|
||||||
|
|
||||||
|
final videocallPushServiceProvider = Provider<VideocallPushService>((ref) {
|
||||||
|
return GetIt.I<VideocallPushService>();
|
||||||
|
});
|
||||||
|
|
||||||
|
final videocallNetServiceProvider = Provider<VideocallNetService>((ref) {
|
||||||
|
return GetIt.I<VideocallNetService>();
|
||||||
|
});
|
||||||
|
|
||||||
|
// -- Stream providers (for reactive UI consumption) --
|
||||||
|
|
||||||
|
final videocallClientStateProvider =
|
||||||
|
StreamProvider<VideocallClientState>((ref) {
|
||||||
|
return ref.watch(videocallClientProvider).stateStream;
|
||||||
|
});
|
||||||
|
|
||||||
|
final videocallCurrentCallProvider = StreamProvider<VideocallItem?>((ref) {
|
||||||
|
final callService = ref.watch(videocallCallServiceProvider);
|
||||||
|
|
||||||
|
Stream<VideocallItem?> stream() async* {
|
||||||
|
yield callService.currentItem;
|
||||||
|
yield* callService.callItemUpdateStream
|
||||||
|
.map((_) => callService.currentItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
return stream();
|
||||||
|
});
|
||||||
@@ -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<VideocallItem>.broadcast();
|
||||||
|
Stream<VideocallItem> get callItemAddStream => _callItemAddController.stream;
|
||||||
|
|
||||||
|
final _callItemUpdateController = StreamController<VideocallItem>.broadcast();
|
||||||
|
Stream<VideocallItem> get callItemUpdateStream =>
|
||||||
|
_callItemUpdateController.stream;
|
||||||
|
|
||||||
|
final _callItemRemoveController =
|
||||||
|
StreamController<({int reason, String description})>.broadcast();
|
||||||
|
Stream<({int reason, String description})> get callItemRemoveStream =>
|
||||||
|
_callItemRemoveController.stream;
|
||||||
|
|
||||||
|
final _missedCallController = StreamController<VideocallItem>.broadcast();
|
||||||
|
Stream<VideocallItem> 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<JCCallItem>.broadcast();
|
||||||
|
Stream<JCCallItem> get earlyMediaStream => _earlyMediaController.stream;
|
||||||
|
|
||||||
|
VideocallItem? _currentItem;
|
||||||
|
VideocallItem? get currentItem => _currentItem;
|
||||||
|
|
||||||
|
// -- Lifecycle --
|
||||||
|
|
||||||
|
Future<bool> 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<bool> destroy() async => JCCall.destroy();
|
||||||
|
|
||||||
|
// -- Call actions --
|
||||||
|
|
||||||
|
Future<bool> startCall({
|
||||||
|
required String userId,
|
||||||
|
required bool isVideo,
|
||||||
|
CallParam? callParam,
|
||||||
|
}) async {
|
||||||
|
if (_call == null) return false;
|
||||||
|
return _call!.call(userId, isVideo, callParam);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> answerCall({required bool isVideo}) async {
|
||||||
|
final item = await _call?.getActiveCallItem();
|
||||||
|
if (item == null) return false;
|
||||||
|
return _call!.answer(item, isVideo);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> 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<bool> termCall(
|
||||||
|
JCCallItem item, int reason, String description) async {
|
||||||
|
if (_call == null) return false;
|
||||||
|
return _call!.term(item, reason, description);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Mute/Audio --
|
||||||
|
|
||||||
|
Future<bool> mute(JCCallItem item) async {
|
||||||
|
if (_call == null) return false;
|
||||||
|
return _call!.mute(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> muteMicrophone(JCCallItem item, bool mute) async {
|
||||||
|
if (_call == null) return false;
|
||||||
|
return _call!.muteMicrophone(item, mute);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> muteSpeaker(JCCallItem item, bool mute) async {
|
||||||
|
if (_call == null) return false;
|
||||||
|
return _call!.muteSpeaker(item, mute);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> setMicScale(JCCallItem item, int scale) async {
|
||||||
|
if (_call == null) return false;
|
||||||
|
return _call!.setMicScale(item, scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Hold --
|
||||||
|
|
||||||
|
Future<bool> hold(JCCallItem item) async {
|
||||||
|
if (_call == null) return false;
|
||||||
|
return _call!.hold(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> becomeActive(JCCallItem item) async {
|
||||||
|
if (_call == null) return false;
|
||||||
|
return _call!.becomeActive(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Video --
|
||||||
|
|
||||||
|
Future<bool> enableUploadVideoStream(JCCallItem item) async {
|
||||||
|
if (_call == null) return false;
|
||||||
|
return _call!.enableUploadVideoStream(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<JCMediaDeviceVideoCanvas?> startLocalVideo(
|
||||||
|
{int renderType = JCMediaDevice.RENDER_FULL_AUTO}) async {
|
||||||
|
final item = await _call?.getActiveCallItem();
|
||||||
|
if (item == null) return null;
|
||||||
|
return item.startSelfVideo(renderType);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> stopLocalVideo() async {
|
||||||
|
final item = await _call?.getActiveCallItem();
|
||||||
|
if (item == null) return false;
|
||||||
|
return item.stopSelfVideo();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<JCMediaDeviceVideoCanvas?> startRemoteVideo(
|
||||||
|
{int renderType = JCMediaDevice.RENDER_FULL_CONTENT}) async {
|
||||||
|
final item = await _call?.getActiveCallItem();
|
||||||
|
if (item == null) return null;
|
||||||
|
return item.startOtherVideo(renderType);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> stopRemoteVideo() async {
|
||||||
|
final item = await _call?.getActiveCallItem();
|
||||||
|
if (item == null) return false;
|
||||||
|
return item.stopOtherVideo();
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Recording --
|
||||||
|
|
||||||
|
Future<bool> audioRecord(
|
||||||
|
JCCallItem item, bool enable, String filePath) async {
|
||||||
|
if (_call == null) return false;
|
||||||
|
return _call!.audioRecord(item, enable, filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> 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<bool> sendMessage(
|
||||||
|
JCCallItem item, String type, String content) async {
|
||||||
|
if (_call == null) return false;
|
||||||
|
return _call!.sendMessage(item, type, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> sendDtmf(JCCallItem item, int value) async {
|
||||||
|
if (_call == null) return false;
|
||||||
|
return _call!.sendDtmf(item, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Items --
|
||||||
|
|
||||||
|
Future<List<JCCallItem>?> getCallItems() async => _call?.getCallItems();
|
||||||
|
|
||||||
|
Future<JCCallItem?> getActiveCallItem() async =>
|
||||||
|
_call?.getActiveCallItem();
|
||||||
|
|
||||||
|
// -- Config --
|
||||||
|
|
||||||
|
Future<String> getStatistics() async {
|
||||||
|
if (_call == null) return '';
|
||||||
|
return _call!.getStatistics();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> updateMediaConfig(MediaConfig mediaConfig) async {
|
||||||
|
if (_call == null) return false;
|
||||||
|
return _call!.updateMediaConfig(mediaConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<MediaConfig?> getMediaConfig() async => _call?.getMediaConfig();
|
||||||
|
|
||||||
|
Future<bool> setMaxCallNum(int num) async {
|
||||||
|
if (_call == null) return false;
|
||||||
|
return _call!.setMaxCallNum(num);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> setTermWhenNetDisconnected(bool term) async {
|
||||||
|
if (_call == null) return false;
|
||||||
|
return _call!.setTermWhenNetDisconnected(term);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<MediaConfig> 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) {}
|
||||||
|
}
|
||||||
@@ -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<JCMediaChannelParticipant>.broadcast();
|
||||||
|
Stream<JCMediaChannelParticipant> get participantJoinStream =>
|
||||||
|
_participantJoinController.stream;
|
||||||
|
|
||||||
|
final _participantLeftController =
|
||||||
|
StreamController<JCMediaChannelParticipant>.broadcast();
|
||||||
|
Stream<JCMediaChannelParticipant> get participantLeftStream =>
|
||||||
|
_participantLeftController.stream;
|
||||||
|
|
||||||
|
final _participantUpdateController = StreamController<
|
||||||
|
({
|
||||||
|
JCMediaChannelParticipant participant,
|
||||||
|
ChannelChangeParam changeParam,
|
||||||
|
})>.broadcast();
|
||||||
|
Stream<
|
||||||
|
({
|
||||||
|
JCMediaChannelParticipant participant,
|
||||||
|
ChannelChangeParam changeParam,
|
||||||
|
})> get participantUpdateStream => _participantUpdateController.stream;
|
||||||
|
|
||||||
|
final _participantVolumeChangeController =
|
||||||
|
StreamController<JCMediaChannelParticipant>.broadcast();
|
||||||
|
Stream<JCMediaChannelParticipant> 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<PropChangeParam>.broadcast();
|
||||||
|
Stream<PropChangeParam> 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<bool> 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<bool> destroy() async => JCMediaChannel.destroy();
|
||||||
|
|
||||||
|
// -- Channel actions --
|
||||||
|
|
||||||
|
Future<bool> join(String channelId, {JoinParam? joinParam}) async {
|
||||||
|
if (_channel == null) return false;
|
||||||
|
return _channel!.join(channelId, joinParam);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> leave() async {
|
||||||
|
if (_channel == null) return false;
|
||||||
|
return _channel!.leave();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> stop() async {
|
||||||
|
if (_channel == null) return false;
|
||||||
|
return _channel!.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> query(String channelId) async {
|
||||||
|
if (_channel == null) return -1;
|
||||||
|
return _channel!.query(channelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Audio/Video streams --
|
||||||
|
|
||||||
|
Future<bool> enableUploadAudioStream(bool enable) async {
|
||||||
|
if (_channel == null) return false;
|
||||||
|
return _channel!.enableUploadAudioStream(enable);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> enableUploadVideoStream(bool enable) async {
|
||||||
|
if (_channel == null) return false;
|
||||||
|
return _channel!.enableUploadVideoStream(enable);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> enableAudioOutput(bool enable) async {
|
||||||
|
if (_channel == null) return false;
|
||||||
|
return _channel!.enableAudioOutput(enable);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> requestVideo(
|
||||||
|
JCMediaChannelParticipant participant, int pictureSize) async {
|
||||||
|
if (_channel == null) return false;
|
||||||
|
return _channel!.requestVideo(participant, pictureSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> requestScreenVideo(String screenUri, int pictureSize) async {
|
||||||
|
if (_channel == null) return false;
|
||||||
|
return _channel!.requestScreenVideo(screenUri, pictureSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Screen share / CDN / Recording --
|
||||||
|
|
||||||
|
Future<bool> enableScreenShare(
|
||||||
|
bool enable, ScreenShareParam? screenShareParam) async {
|
||||||
|
if (_channel == null) return false;
|
||||||
|
return _channel!.enableScreenShare(enable, screenShareParam);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> enableCdn(bool enable, int keyInterval) async {
|
||||||
|
if (_channel == null) return false;
|
||||||
|
return _channel!.enableCdn(enable, keyInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> enableRecord(bool enable, {RecordParam? recordParam}) async {
|
||||||
|
if (_channel == null) return false;
|
||||||
|
return _channel!.enableRecord(enable, recordParam);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Participants --
|
||||||
|
|
||||||
|
Future<List<JCMediaChannelParticipant>?> getParticipants() async =>
|
||||||
|
_channel?.getParticipants();
|
||||||
|
|
||||||
|
Future<JCMediaChannelParticipant?> getParticipant(String userId) async =>
|
||||||
|
_channel?.getParticipant(userId);
|
||||||
|
|
||||||
|
Future<bool> kick(JCMediaChannelParticipant participant) async {
|
||||||
|
if (_channel == null) return false;
|
||||||
|
return _channel!.kick(participant);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> inviteSipUser(String userId, {SipParam? sipParam}) async {
|
||||||
|
if (_channel == null) return -1;
|
||||||
|
return _channel!.inviteSipUser(userId, sipParam);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Custom roles/state --
|
||||||
|
|
||||||
|
Future<bool> setCustomRole(int customRole,
|
||||||
|
{JCMediaChannelParticipant? participant}) async {
|
||||||
|
if (_channel == null) return false;
|
||||||
|
return _channel!.setCustomRole(customRole, participant);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> getCustomRole() async {
|
||||||
|
if (_channel == null) return 0;
|
||||||
|
return _channel!.getCustomRole();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> setCustomState(int customState,
|
||||||
|
{JCMediaChannelParticipant? participant}) async {
|
||||||
|
if (_channel == null) return false;
|
||||||
|
return _channel!.setCustomState(customState, participant);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> getCustomState() async {
|
||||||
|
if (_channel == null) return 0;
|
||||||
|
return _channel!.getCustomState();
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Channel properties --
|
||||||
|
|
||||||
|
Future<int> setCustomProperty(String property) async {
|
||||||
|
if (_channel == null) return -1;
|
||||||
|
return _channel!.setCustomProperty(property);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> getCustomProperty() async {
|
||||||
|
if (_channel == null) return '';
|
||||||
|
return _channel!.getCustomProperty();
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Channel info --
|
||||||
|
|
||||||
|
Future<String> getChannelUri() async {
|
||||||
|
if (_channel == null) return '';
|
||||||
|
return _channel!.getChannelUri();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> getChannelId() async {
|
||||||
|
if (_channel == null) return '';
|
||||||
|
return _channel!.getChannelId();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> getSessionId() async {
|
||||||
|
if (_channel == null) return '';
|
||||||
|
return _channel!.getSessionId();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> getConfId() async {
|
||||||
|
if (_channel == null) return -1;
|
||||||
|
return _channel!.getConfId();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> getPassword() async {
|
||||||
|
if (_channel == null) return '';
|
||||||
|
return _channel!.getPassword();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> getChannelNumber() async {
|
||||||
|
if (_channel == null) return 0;
|
||||||
|
return _channel!.getChannelNumber();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> getTitle() async {
|
||||||
|
if (_channel == null) return '';
|
||||||
|
return _channel!.getTitle();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> getChannelState() async {
|
||||||
|
if (_channel == null) return JCMediaChannel.STATE_IDLE;
|
||||||
|
return _channel!.getState();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> getUploadLocalAudio() async {
|
||||||
|
if (_channel == null) return false;
|
||||||
|
return _channel!.getUploadLocalAudio();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> getUploadLocalVideo() async {
|
||||||
|
if (_channel == null) return false;
|
||||||
|
return _channel!.getUploadLocalVideo();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> getAudioOutput() async {
|
||||||
|
if (_channel == null) return false;
|
||||||
|
return _channel!.getAudioOutput();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> getRecordState() async {
|
||||||
|
if (_channel == null) return JCMediaChannel.RECORD_STATE_NONE;
|
||||||
|
return _channel!.getRecordState();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> getCdnState() async {
|
||||||
|
if (_channel == null) return JCMediaChannel.CDN_STATE_NONE;
|
||||||
|
return _channel!.getCdnState();
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Messaging --
|
||||||
|
|
||||||
|
Future<bool> sendMessage(
|
||||||
|
String type, String content, String toUserId) async {
|
||||||
|
if (_channel == null) return false;
|
||||||
|
return _channel!.sendMessage(type, content, toUserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> sendCommand(String name, String param) async {
|
||||||
|
if (_channel == null) return false;
|
||||||
|
return _channel!.sendCommand(name, param);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Statistics --
|
||||||
|
|
||||||
|
Future<String> 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));
|
||||||
|
}
|
||||||
247
packages/videocall_sdk/lib/src/services/videocall_client.dart
Normal file
247
packages/videocall_sdk/lib/src/services/videocall_client.dart
Normal file
@@ -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<VideocallClientState>.broadcast();
|
||||||
|
Stream<VideocallClientState> get stateStream => _stateController.stream;
|
||||||
|
|
||||||
|
final _loginResultController =
|
||||||
|
StreamController<({bool success, LoginFailureReason? reason})>.broadcast();
|
||||||
|
Stream<({bool success, LoginFailureReason? reason})> get loginResultStream =>
|
||||||
|
_loginResultController.stream;
|
||||||
|
|
||||||
|
final _logoutController = StreamController<int>.broadcast();
|
||||||
|
Stream<int> 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<bool> 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<bool> destroy() async {
|
||||||
|
final result = await JCClient.destroy();
|
||||||
|
if (result) {
|
||||||
|
_client = null;
|
||||||
|
_updateState(VideocallClientState.notInitialized);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Auth --
|
||||||
|
|
||||||
|
Future<bool> 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<bool> relogin({
|
||||||
|
required String userId,
|
||||||
|
required String password,
|
||||||
|
LoginParam? loginParam,
|
||||||
|
}) async {
|
||||||
|
if (_client == null) return false;
|
||||||
|
return _client!.relogin(userId, password, loginParam ?? LoginParam());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> logout() async {
|
||||||
|
if (_client == null) return false;
|
||||||
|
return _client!.logout();
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- User info --
|
||||||
|
|
||||||
|
Future<String?> getUserId() async => _client?.getUserId();
|
||||||
|
|
||||||
|
Future<String?> getDisplayName() async => _client?.getDisplayName();
|
||||||
|
|
||||||
|
Future<bool> setDisplayName(String displayName) async {
|
||||||
|
if (_client == null) return false;
|
||||||
|
return _client!.setDisplayName(displayName);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> getAppKey() async => _client?.getAppkey();
|
||||||
|
|
||||||
|
Future<String> getServerUid() async {
|
||||||
|
if (_client == null) return '';
|
||||||
|
return _client!.getServerUid();
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Server config --
|
||||||
|
|
||||||
|
Future<bool> setServerAddress(String serverAddress) async {
|
||||||
|
if (_client == null) return false;
|
||||||
|
return _client!.setServerAddress(serverAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> getServerAddress() async {
|
||||||
|
if (_client == null) return '';
|
||||||
|
return _client!.getServerAddress();
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Foreground/background --
|
||||||
|
|
||||||
|
Future<bool> setForeground(bool foreground) async {
|
||||||
|
if (_client == null) return false;
|
||||||
|
return _client!.setForeground(foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Online messaging --
|
||||||
|
|
||||||
|
Future<int> sendOnlineMessage({
|
||||||
|
required String userId,
|
||||||
|
required String content,
|
||||||
|
}) async {
|
||||||
|
if (_client == null) return -1;
|
||||||
|
return _client!.sendOnlineMessage(userId, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Params --
|
||||||
|
|
||||||
|
Future<CreateParam?> getCreateParam() async => _client?.getCreateParam();
|
||||||
|
|
||||||
|
Future<LoginParam?> getLoginParam() async => _client?.getLoginParam();
|
||||||
|
|
||||||
|
Future<int> 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<void>.broadcast();
|
||||||
|
Stream<void> get cameraUpdateStream => _cameraUpdateController.stream;
|
||||||
|
|
||||||
|
final _audioOutputTypeController = StreamController<int>.broadcast();
|
||||||
|
Stream<int> get audioOutputTypeStream => _audioOutputTypeController.stream;
|
||||||
|
|
||||||
|
final _renderReceivedController =
|
||||||
|
StreamController<JCMediaDeviceVideoCanvas>.broadcast();
|
||||||
|
Stream<JCMediaDeviceVideoCanvas> get renderReceivedStream =>
|
||||||
|
_renderReceivedController.stream;
|
||||||
|
|
||||||
|
final _renderStartController =
|
||||||
|
StreamController<JCMediaDeviceVideoCanvas>.broadcast();
|
||||||
|
Stream<JCMediaDeviceVideoCanvas> get renderStartStream =>
|
||||||
|
_renderStartController.stream;
|
||||||
|
|
||||||
|
final _videoErrorController =
|
||||||
|
StreamController<JCMediaDeviceVideoCanvas>.broadcast();
|
||||||
|
Stream<JCMediaDeviceVideoCanvas> get videoErrorStream =>
|
||||||
|
_videoErrorController.stream;
|
||||||
|
|
||||||
|
final _audioErrorController = StreamController<bool>.broadcast();
|
||||||
|
Stream<bool> get audioErrorStream => _audioErrorController.stream;
|
||||||
|
|
||||||
|
final _audioResumeController = StreamController<void>.broadcast();
|
||||||
|
Stream<void> get audioResumeStream => _audioResumeController.stream;
|
||||||
|
|
||||||
|
// -- Lifecycle --
|
||||||
|
|
||||||
|
Future<bool> initialize() async {
|
||||||
|
final client = _clientRef.client;
|
||||||
|
if (client == null) return false;
|
||||||
|
_mediaDevice = await JCMediaDevice.create(client, this);
|
||||||
|
return _mediaDevice != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> destroy() async => JCMediaDevice.destroy();
|
||||||
|
|
||||||
|
// -- Camera --
|
||||||
|
|
||||||
|
Future<bool> isCameraOpen() async {
|
||||||
|
if (_mediaDevice == null) return false;
|
||||||
|
return _mediaDevice!.isCameraOpen();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<JCMediaDeviceCamera?> getCamera() async => _mediaDevice?.getCamera();
|
||||||
|
|
||||||
|
Future<List<JCMediaDeviceCamera>?> getCameras() async =>
|
||||||
|
_mediaDevice?.getCameras();
|
||||||
|
|
||||||
|
Future<List<JCMediaDeviceCamera>?> getExtCameras() async =>
|
||||||
|
_mediaDevice?.getExtCameras();
|
||||||
|
|
||||||
|
Future<JCMediaDeviceCamera?> getDefaultCamera() async =>
|
||||||
|
_mediaDevice?.getDefaultCamera();
|
||||||
|
|
||||||
|
Future<bool> setDefaultCamera(String cameraId) async {
|
||||||
|
if (_mediaDevice == null) return false;
|
||||||
|
return _mediaDevice!.setDefaultCamera(cameraId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> startCamera() async {
|
||||||
|
if (_mediaDevice == null) return false;
|
||||||
|
return _mediaDevice!.startCamera();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> stopCamera() async {
|
||||||
|
if (_mediaDevice == null) return false;
|
||||||
|
return _mediaDevice!.stopCamera();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> switchCamera({JCMediaDeviceCamera? camera}) async {
|
||||||
|
if (_mediaDevice == null) return false;
|
||||||
|
return _mediaDevice!.switchCamera(camera: camera);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> setCameraProperty(int width, int height, int frameRate) async {
|
||||||
|
if (_mediaDevice == null) return false;
|
||||||
|
return _mediaDevice!.setCameraProperty(width, height, frameRate);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> setScreenCaptureProperty(
|
||||||
|
int width, int height, int frameRate) async {
|
||||||
|
if (_mediaDevice == null) return false;
|
||||||
|
return _mediaDevice!.setScreenCaptureProperty(width, height, frameRate);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> getCameraType(int cameraIndex) async {
|
||||||
|
if (_mediaDevice == null) return JCMediaDevice.CAMERA_NONE;
|
||||||
|
return _mediaDevice!.getCameraType(cameraIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Exposure --
|
||||||
|
|
||||||
|
Future<int> getMinExposureCompensation() async {
|
||||||
|
if (_mediaDevice == null) return 0;
|
||||||
|
return _mediaDevice!.getMinExposureCompensation();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> getMaxExposureCompensation() async {
|
||||||
|
if (_mediaDevice == null) return 0;
|
||||||
|
return _mediaDevice!.getMaxExposureCompensation();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<double> getExposureCompensationStep() async {
|
||||||
|
if (_mediaDevice == null) return 0;
|
||||||
|
return _mediaDevice!.getExposureCompensationStep();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> setIOSExposureCompensation(double level) async {
|
||||||
|
if (_mediaDevice == null) return false;
|
||||||
|
return _mediaDevice!.setIOSExposureCompensation(level);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> setExposureCompensation(int level) async {
|
||||||
|
if (_mediaDevice == null) return false;
|
||||||
|
return _mediaDevice!.setExposureCompensation(level);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Flash --
|
||||||
|
|
||||||
|
Future<bool> isCameraFlashSupported() async {
|
||||||
|
if (_mediaDevice == null) return false;
|
||||||
|
return _mediaDevice!.isCameraFlashSupported();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> enableFlash(bool enable) async {
|
||||||
|
if (_mediaDevice == null) return false;
|
||||||
|
return _mediaDevice!.enableFlash(enable);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Focus/Zoom --
|
||||||
|
|
||||||
|
Future<bool> handleFocusMetering(
|
||||||
|
JCMediaDeviceVideoCanvas canvas, double xPercent, double yPercent) async {
|
||||||
|
if (_mediaDevice == null) return false;
|
||||||
|
return _mediaDevice!.handleFocusMetering(canvas, xPercent, yPercent);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> setCameraZoomRatio(double zoomRatio) async {
|
||||||
|
if (_mediaDevice == null) return false;
|
||||||
|
return _mediaDevice!.setCameraZoomRatio(zoomRatio);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<double> getCameraMaxZoom() async {
|
||||||
|
if (_mediaDevice == null) return 1.0;
|
||||||
|
return _mediaDevice!.getCameraMaxZoom();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> getCameraCurrentZoom() async {
|
||||||
|
if (_mediaDevice == null) return 0;
|
||||||
|
return _mediaDevice!.getCameraCurrentZoom();
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Speaker --
|
||||||
|
|
||||||
|
Future<bool> isSpeakerOn() async {
|
||||||
|
if (_mediaDevice == null) return false;
|
||||||
|
return _mediaDevice!.isSpeakerOn();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> enableSpeaker(bool enable) async {
|
||||||
|
if (_mediaDevice == null) return false;
|
||||||
|
return _mediaDevice!.enableSpeaker(enable);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> getDefaultSpeakerOn() async {
|
||||||
|
if (_mediaDevice == null) return false;
|
||||||
|
return _mediaDevice!.getDefaultSpeakerOn();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> setDefaultSpeakerOn(bool state) async {
|
||||||
|
if (_mediaDevice == null) return false;
|
||||||
|
return _mediaDevice!.setDefaultSpeakerOn(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> getAudioRouteType() async {
|
||||||
|
if (_mediaDevice == null) return 0;
|
||||||
|
return _mediaDevice!.getAudioRouteType();
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Audio --
|
||||||
|
|
||||||
|
Future<bool> isAudioStart() async {
|
||||||
|
if (_mediaDevice == null) return false;
|
||||||
|
return _mediaDevice!.isAudioStart();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> startAudio() async {
|
||||||
|
if (_mediaDevice == null) return false;
|
||||||
|
return _mediaDevice!.startAudio();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> stopAudio() async {
|
||||||
|
if (_mediaDevice == null) return false;
|
||||||
|
return _mediaDevice!.stopAudio();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> setAudioAecMode(int mode) async {
|
||||||
|
if (_mediaDevice == null) return false;
|
||||||
|
return _mediaDevice!.setAudioAecMode(mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> autoStartAudioOutput(bool enable) async {
|
||||||
|
if (_mediaDevice == null) return false;
|
||||||
|
return _mediaDevice!.autoStartAudioOutput(enable);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> autoStartAudioInput(bool enable) async {
|
||||||
|
if (_mediaDevice == null) return false;
|
||||||
|
return _mediaDevice!.autoStartAudioInput(enable);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> setDeviceAudioAutoInput(bool auto) async {
|
||||||
|
if (_mediaDevice == null) return false;
|
||||||
|
return _mediaDevice!.setDeviceAudioAutoInput(auto);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> setDeviceAudioAutoOutput(bool auto) async {
|
||||||
|
if (_mediaDevice == null) return false;
|
||||||
|
return _mediaDevice!.setDeviceAudioAutoOutput(auto);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> getUseInternalAudioDeviceLogic() async {
|
||||||
|
if (_mediaDevice == null) return false;
|
||||||
|
return _mediaDevice!.getUseInternalAudioDeviceLogic();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<JCMediaDeviceAudioParam?> getAudioParam() async =>
|
||||||
|
_mediaDevice?.getAudioParam();
|
||||||
|
|
||||||
|
// -- Volume --
|
||||||
|
|
||||||
|
Future<int> getOutputVolume() async {
|
||||||
|
if (_mediaDevice == null) return 0;
|
||||||
|
return _mediaDevice!.getOutputVolume();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> 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<JCMediaDeviceVideoCanvas?> startCameraVideo(
|
||||||
|
{int renderType = JCMediaDevice.RENDER_FULL_AUTO}) async {
|
||||||
|
return _mediaDevice?.startCameraVideo(renderType);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<JCMediaDeviceVideoCanvas?> startVideo(
|
||||||
|
String? videoSource, int renderType) async {
|
||||||
|
return _mediaDevice?.startVideo(videoSource, renderType);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> stopVideo(JCMediaDeviceVideoCanvas canvas) async {
|
||||||
|
if (_mediaDevice == null) return false;
|
||||||
|
return _mediaDevice!.stopVideo(canvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Video file (custom capture) --
|
||||||
|
|
||||||
|
Future<bool> isVideoFileOpen() async {
|
||||||
|
if (_mediaDevice == null) return false;
|
||||||
|
return _mediaDevice!.isVideoFileOpen();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> getVideoFileId() async {
|
||||||
|
if (_mediaDevice == null) return '';
|
||||||
|
return _mediaDevice!.getVideoFileId();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> startVideoFile() async {
|
||||||
|
if (_mediaDevice == null) return false;
|
||||||
|
return _mediaDevice!.startVideoFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> 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<bool> stopVideoFile() async {
|
||||||
|
if (_mediaDevice == null) return false;
|
||||||
|
return _mediaDevice!.stopVideoFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Video angle --
|
||||||
|
|
||||||
|
Future<bool> setVideoAngle(int angle) async {
|
||||||
|
if (_mediaDevice == null) return false;
|
||||||
|
return _mediaDevice!.setVideoAngle(angle);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> getVideoAngle() async {
|
||||||
|
if (_mediaDevice == null) return 0;
|
||||||
|
return _mediaDevice!.getVideoAngle();
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Frame callbacks --
|
||||||
|
|
||||||
|
Future<bool> setAudioFrameCallback(JCAudioFrameCallback? callback) async {
|
||||||
|
if (_mediaDevice == null) return false;
|
||||||
|
return _mediaDevice!.setAudioFrameCallback(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> setVideoFrameCallback(JCVideoFrameCallback? callback) async {
|
||||||
|
if (_mediaDevice == null) return false;
|
||||||
|
return _mediaDevice!.setVideoFrameCallback(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Custom audio --
|
||||||
|
|
||||||
|
Future<bool> 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<Uint8List?> 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() {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import 'package:jc_sdk/jc_sdk.dart';
|
||||||
|
|
||||||
|
class VideocallLogService {
|
||||||
|
VideocallLogService._();
|
||||||
|
|
||||||
|
static Future<bool> uploadLog(String reason) => JCLog.uploadLog(reason);
|
||||||
|
static Future<bool> info(String type, String format) =>
|
||||||
|
JCLog.info(type, format);
|
||||||
|
static Future<bool> error(String type, String format) =>
|
||||||
|
JCLog.error(type, format);
|
||||||
|
static Future<bool> debug(String type, String format) =>
|
||||||
|
JCLog.debug(type, format);
|
||||||
|
}
|
||||||
@@ -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<int> getNetType() async => JCNet.getInstance().getNetType();
|
||||||
|
|
||||||
|
Future<bool> 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<bool> initialize() async {
|
||||||
|
final client = _clientRef.client;
|
||||||
|
if (client == null) return false;
|
||||||
|
_push = await JCPush.create(client);
|
||||||
|
return _push != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> destroy() async {
|
||||||
|
await JCPush.destroy();
|
||||||
|
_push = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Push --
|
||||||
|
|
||||||
|
Future<bool> addPushInfo(JCPushTemplate info) async {
|
||||||
|
if (_push == null) return false;
|
||||||
|
return _push!.addPushInfo(info);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> addPushTemplate(String data) async {
|
||||||
|
if (_push == null) return false;
|
||||||
|
return _push!.addPushTemplate(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Dispose --
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
_push = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
60
packages/videocall_sdk/lib/videocall_sdk.dart
Normal file
60
packages/videocall_sdk/lib/videocall_sdk.dart
Normal file
@@ -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;
|
||||||
21
packages/videocall_sdk/pubspec.yaml
Normal file
21
packages/videocall_sdk/pubspec.yaml
Normal file
@@ -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
|
||||||
@@ -1004,6 +1004,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.5"
|
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:
|
js:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ workspace:
|
|||||||
- packages/sf_shared
|
- packages/sf_shared
|
||||||
- packages/sf_tracking
|
- packages/sf_tracking
|
||||||
- packages/utils
|
- packages/utils
|
||||||
|
- packages/videocall_sdk
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter_secure_storage: ^9.2.4
|
flutter_secure_storage: ^9.2.4
|
||||||
|
|||||||
Reference in New Issue
Block a user