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:
2026-04-16 17:45:33 +02:00
parent e7ebe7f403
commit 4347cefaed
32 changed files with 1885 additions and 4 deletions

View File

@@ -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"
}

View File

@@ -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"
}
"wsUrl": "wss://api-platform.savefamily.app/websocket",
"juphoonAppKey": "9efcf2d889dc8a0320925096"
}

View File

@@ -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"
}

View File

@@ -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');

View 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;
}

View File

@@ -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<void> initApp(EnvironmentEnum env) async {
navigationModule();
scaTreezorModule();
videocallSdkModule(SaveFamilyVideocallConfig());
themePackages();
await setupFirebase(env);

View File

@@ -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

31
packages/videocall_sdk/.gitignore vendored Normal file
View 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/

View File

@@ -0,0 +1,3 @@
## 0.0.1
* Initial release. Wrapper around Juphoon jc_sdk for video calling.

View File

@@ -0,0 +1 @@
TODO: Add your license here.

View 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.

View 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

View File

@@ -0,0 +1,7 @@
import 'package:jc_sdk/jc_sdk.dart';
abstract class VideocallSdkConfig {
String get appKey;
String get serverAddress;
CreateParam? get createParam;
}

View 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>(),
),
);
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,4 @@
enum CallDirection {
incoming,
outgoing,
}

View File

@@ -0,0 +1,9 @@
enum VideocallState {
idle,
pending,
connecting,
talking,
ok,
canceled,
missed,
}

View File

@@ -0,0 +1,12 @@
enum LoginFailureReason {
auth,
network,
appkey,
noUser,
timeout,
serverLogout,
anotherDeviceLoggedIn,
tokenMismatch,
tokenExpired,
unknown,
}

View File

@@ -0,0 +1,7 @@
enum VideocallClientState {
notInitialized,
idle,
loggingIn,
loggedIn,
loggingOut,
}

View 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,
);
}
}

View File

@@ -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();
});

View File

@@ -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) {}
}

View File

@@ -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));
}

View 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));
}
}

View File

@@ -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() {}
}

View File

@@ -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);
}

View File

@@ -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));
}
}

View File

@@ -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;
}
}

View 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;

View 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

View File

@@ -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:

View File

@@ -36,6 +36,7 @@ workspace:
- packages/sf_shared
- packages/sf_tracking
- packages/utils
- packages/videocall_sdk
dependencies:
flutter_secure_storage: ^9.2.4