feat(videocall): complete signaling integration, group call support, and UI polish
- Wire VIDEO_CALL_REQUEST/CANCEL/REFUSE/ROOM_COUNT commands via CommandsRepository - Add VideocallChatType enum (single/multi) with chatType stored in state - Implement auto-login to Juphoon SDK using sanitized email + user UUID - Add runtime camera/microphone permissions before call start - Add RetryInterceptor for transient TLS/socket errors in Dio - Migrate VideocallItem to Freezed with isTalking extension - Implement startGroupCall/leaveGroupCall using ChannelService with participant grid - Add PopScope to intercept back navigation during active calls - Redesign idle screen with device option cards and group call button - Redesign active call UI with video overlay, PiP local view, and new controls layout - Clean up SDK wrapper: remove unused streams, merge destroy+dispose into shutdown - Add i18n keys for videocall UI across 6 locales
This commit is contained in:
@@ -49,10 +49,9 @@ class VideocallSdkManager {
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<void> destroy() async {
|
||||
Future<void> shutdown() async {
|
||||
if (!_initialized) return;
|
||||
|
||||
// Reverse order
|
||||
await pushService.destroy();
|
||||
await channelService.destroy();
|
||||
await callService.destroy();
|
||||
@@ -60,16 +59,13 @@ class VideocallSdkManager {
|
||||
await deviceService.destroy();
|
||||
await client.destroy();
|
||||
|
||||
_initialized = false;
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
callService.dispose();
|
||||
channelService.dispose();
|
||||
pushService.dispose();
|
||||
netService.dispose();
|
||||
deviceService.dispose();
|
||||
client.dispose();
|
||||
|
||||
_initialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,42 +1,22 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import 'call_direction.dart';
|
||||
import 'call_state.dart';
|
||||
|
||||
class VideocallItem {
|
||||
const VideocallItem({
|
||||
required this.userId,
|
||||
required this.isVideo,
|
||||
required this.direction,
|
||||
required this.state,
|
||||
this.uploadVideoStreamSelf = false,
|
||||
this.uploadVideoStreamOther = false,
|
||||
});
|
||||
part 'videocall_item.freezed.dart';
|
||||
|
||||
final String userId;
|
||||
final bool isVideo;
|
||||
final CallDirection direction;
|
||||
final VideocallState state;
|
||||
final bool uploadVideoStreamSelf;
|
||||
final bool uploadVideoStreamOther;
|
||||
|
||||
bool get isTalking => state == VideocallState.talking;
|
||||
|
||||
VideocallItem copyWith({
|
||||
String? userId,
|
||||
bool? isVideo,
|
||||
CallDirection? direction,
|
||||
VideocallState? state,
|
||||
bool? uploadVideoStreamSelf,
|
||||
bool? uploadVideoStreamOther,
|
||||
}) {
|
||||
return VideocallItem(
|
||||
userId: userId ?? this.userId,
|
||||
isVideo: isVideo ?? this.isVideo,
|
||||
direction: direction ?? this.direction,
|
||||
state: state ?? this.state,
|
||||
uploadVideoStreamSelf:
|
||||
uploadVideoStreamSelf ?? this.uploadVideoStreamSelf,
|
||||
uploadVideoStreamOther:
|
||||
uploadVideoStreamOther ?? this.uploadVideoStreamOther,
|
||||
);
|
||||
}
|
||||
@freezed
|
||||
abstract class VideocallItem with _$VideocallItem {
|
||||
const factory VideocallItem({
|
||||
required String userId,
|
||||
required bool isVideo,
|
||||
required CallDirection direction,
|
||||
required VideocallState state,
|
||||
@Default(false) bool uploadVideoStreamSelf,
|
||||
@Default(false) bool uploadVideoStreamOther,
|
||||
}) = _VideocallItem;
|
||||
}
|
||||
|
||||
extension VideocallItemX on VideocallItem {
|
||||
bool get isTalking => state == VideocallState.talking;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'videocall_item.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
mixin _$VideocallItem {
|
||||
|
||||
String get userId; bool get isVideo; CallDirection get direction; VideocallState get state; bool get uploadVideoStreamSelf; bool get uploadVideoStreamOther;
|
||||
/// Create a copy of VideocallItem
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$VideocallItemCopyWith<VideocallItem> get copyWith => _$VideocallItemCopyWithImpl<VideocallItem>(this as VideocallItem, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is VideocallItem&&(identical(other.userId, userId) || other.userId == userId)&&(identical(other.isVideo, isVideo) || other.isVideo == isVideo)&&(identical(other.direction, direction) || other.direction == direction)&&(identical(other.state, state) || other.state == state)&&(identical(other.uploadVideoStreamSelf, uploadVideoStreamSelf) || other.uploadVideoStreamSelf == uploadVideoStreamSelf)&&(identical(other.uploadVideoStreamOther, uploadVideoStreamOther) || other.uploadVideoStreamOther == uploadVideoStreamOther));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,userId,isVideo,direction,state,uploadVideoStreamSelf,uploadVideoStreamOther);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'VideocallItem(userId: $userId, isVideo: $isVideo, direction: $direction, state: $state, uploadVideoStreamSelf: $uploadVideoStreamSelf, uploadVideoStreamOther: $uploadVideoStreamOther)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $VideocallItemCopyWith<$Res> {
|
||||
factory $VideocallItemCopyWith(VideocallItem value, $Res Function(VideocallItem) _then) = _$VideocallItemCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String userId, bool isVideo, CallDirection direction, VideocallState state, bool uploadVideoStreamSelf, bool uploadVideoStreamOther
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$VideocallItemCopyWithImpl<$Res>
|
||||
implements $VideocallItemCopyWith<$Res> {
|
||||
_$VideocallItemCopyWithImpl(this._self, this._then);
|
||||
|
||||
final VideocallItem _self;
|
||||
final $Res Function(VideocallItem) _then;
|
||||
|
||||
/// Create a copy of VideocallItem
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? userId = null,Object? isVideo = null,Object? direction = null,Object? state = null,Object? uploadVideoStreamSelf = null,Object? uploadVideoStreamOther = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
userId: null == userId ? _self.userId : userId // ignore: cast_nullable_to_non_nullable
|
||||
as String,isVideo: null == isVideo ? _self.isVideo : isVideo // ignore: cast_nullable_to_non_nullable
|
||||
as bool,direction: null == direction ? _self.direction : direction // ignore: cast_nullable_to_non_nullable
|
||||
as CallDirection,state: null == state ? _self.state : state // ignore: cast_nullable_to_non_nullable
|
||||
as VideocallState,uploadVideoStreamSelf: null == uploadVideoStreamSelf ? _self.uploadVideoStreamSelf : uploadVideoStreamSelf // ignore: cast_nullable_to_non_nullable
|
||||
as bool,uploadVideoStreamOther: null == uploadVideoStreamOther ? _self.uploadVideoStreamOther : uploadVideoStreamOther // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [VideocallItem].
|
||||
extension VideocallItemPatterns on VideocallItem {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _VideocallItem value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _VideocallItem() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _VideocallItem value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _VideocallItem():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _VideocallItem value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _VideocallItem() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String userId, bool isVideo, CallDirection direction, VideocallState state, bool uploadVideoStreamSelf, bool uploadVideoStreamOther)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _VideocallItem() when $default != null:
|
||||
return $default(_that.userId,_that.isVideo,_that.direction,_that.state,_that.uploadVideoStreamSelf,_that.uploadVideoStreamOther);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String userId, bool isVideo, CallDirection direction, VideocallState state, bool uploadVideoStreamSelf, bool uploadVideoStreamOther) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _VideocallItem():
|
||||
return $default(_that.userId,_that.isVideo,_that.direction,_that.state,_that.uploadVideoStreamSelf,_that.uploadVideoStreamOther);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String userId, bool isVideo, CallDirection direction, VideocallState state, bool uploadVideoStreamSelf, bool uploadVideoStreamOther)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _VideocallItem() when $default != null:
|
||||
return $default(_that.userId,_that.isVideo,_that.direction,_that.state,_that.uploadVideoStreamSelf,_that.uploadVideoStreamOther);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class _VideocallItem implements VideocallItem {
|
||||
const _VideocallItem({required this.userId, required this.isVideo, required this.direction, required this.state, this.uploadVideoStreamSelf = false, this.uploadVideoStreamOther = false});
|
||||
|
||||
|
||||
@override final String userId;
|
||||
@override final bool isVideo;
|
||||
@override final CallDirection direction;
|
||||
@override final VideocallState state;
|
||||
@override@JsonKey() final bool uploadVideoStreamSelf;
|
||||
@override@JsonKey() final bool uploadVideoStreamOther;
|
||||
|
||||
/// Create a copy of VideocallItem
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$VideocallItemCopyWith<_VideocallItem> get copyWith => __$VideocallItemCopyWithImpl<_VideocallItem>(this, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _VideocallItem&&(identical(other.userId, userId) || other.userId == userId)&&(identical(other.isVideo, isVideo) || other.isVideo == isVideo)&&(identical(other.direction, direction) || other.direction == direction)&&(identical(other.state, state) || other.state == state)&&(identical(other.uploadVideoStreamSelf, uploadVideoStreamSelf) || other.uploadVideoStreamSelf == uploadVideoStreamSelf)&&(identical(other.uploadVideoStreamOther, uploadVideoStreamOther) || other.uploadVideoStreamOther == uploadVideoStreamOther));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,userId,isVideo,direction,state,uploadVideoStreamSelf,uploadVideoStreamOther);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'VideocallItem(userId: $userId, isVideo: $isVideo, direction: $direction, state: $state, uploadVideoStreamSelf: $uploadVideoStreamSelf, uploadVideoStreamOther: $uploadVideoStreamOther)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$VideocallItemCopyWith<$Res> implements $VideocallItemCopyWith<$Res> {
|
||||
factory _$VideocallItemCopyWith(_VideocallItem value, $Res Function(_VideocallItem) _then) = __$VideocallItemCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String userId, bool isVideo, CallDirection direction, VideocallState state, bool uploadVideoStreamSelf, bool uploadVideoStreamOther
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$VideocallItemCopyWithImpl<$Res>
|
||||
implements _$VideocallItemCopyWith<$Res> {
|
||||
__$VideocallItemCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _VideocallItem _self;
|
||||
final $Res Function(_VideocallItem) _then;
|
||||
|
||||
/// Create a copy of VideocallItem
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? userId = null,Object? isVideo = null,Object? direction = null,Object? state = null,Object? uploadVideoStreamSelf = null,Object? uploadVideoStreamOther = null,}) {
|
||||
return _then(_VideocallItem(
|
||||
userId: null == userId ? _self.userId : userId // ignore: cast_nullable_to_non_nullable
|
||||
as String,isVideo: null == isVideo ? _self.isVideo : isVideo // ignore: cast_nullable_to_non_nullable
|
||||
as bool,direction: null == direction ? _self.direction : direction // ignore: cast_nullable_to_non_nullable
|
||||
as CallDirection,state: null == state ? _self.state : state // ignore: cast_nullable_to_non_nullable
|
||||
as VideocallState,uploadVideoStreamSelf: null == uploadVideoStreamSelf ? _self.uploadVideoStreamSelf : uploadVideoStreamSelf // ignore: cast_nullable_to_non_nullable
|
||||
as bool,uploadVideoStreamOther: null == uploadVideoStreamOther ? _self.uploadVideoStreamOther : uploadVideoStreamOther // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
@@ -11,8 +11,6 @@ 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>();
|
||||
});
|
||||
@@ -42,8 +40,6 @@ 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;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:jc_sdk/jc_sdk.dart';
|
||||
|
||||
import '../models/call_direction.dart';
|
||||
@@ -12,16 +13,14 @@ class VideocallCallService with JCCallCallback {
|
||||
VideocallCallService({
|
||||
required VideocallClient client,
|
||||
required VideocallDeviceService deviceService,
|
||||
}) : _clientRef = client,
|
||||
_deviceRef = 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;
|
||||
|
||||
@@ -37,25 +36,9 @@ class VideocallCallService with JCCallCallback {
|
||||
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;
|
||||
@@ -66,8 +49,6 @@ class VideocallCallService with JCCallCallback {
|
||||
|
||||
Future<bool> destroy() async => JCCall.destroy();
|
||||
|
||||
// -- Call actions --
|
||||
|
||||
Future<bool> startCall({
|
||||
required String userId,
|
||||
required bool isVideo,
|
||||
@@ -89,14 +70,11 @@ class VideocallCallService with JCCallCallback {
|
||||
return _call!.term(item, reason ?? JCCall.REASON_NONE, description ?? '');
|
||||
}
|
||||
|
||||
Future<bool> termCall(
|
||||
JCCallItem item, int reason, String description) async {
|
||||
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);
|
||||
@@ -117,8 +95,6 @@ class VideocallCallService with JCCallCallback {
|
||||
return _call!.setMicScale(item, scale);
|
||||
}
|
||||
|
||||
// -- Hold --
|
||||
|
||||
Future<bool> hold(JCCallItem item) async {
|
||||
if (_call == null) return false;
|
||||
return _call!.hold(item);
|
||||
@@ -129,15 +105,14 @@ class VideocallCallService with JCCallCallback {
|
||||
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 {
|
||||
Future<JCMediaDeviceVideoCanvas?> startLocalVideo({
|
||||
int renderType = JCMediaDevice.RENDER_FULL_AUTO,
|
||||
}) async {
|
||||
final item = await _call?.getActiveCallItem();
|
||||
if (item == null) return null;
|
||||
return item.startSelfVideo(renderType);
|
||||
@@ -149,8 +124,9 @@ class VideocallCallService with JCCallCallback {
|
||||
return item.stopSelfVideo();
|
||||
}
|
||||
|
||||
Future<JCMediaDeviceVideoCanvas?> startRemoteVideo(
|
||||
{int renderType = JCMediaDevice.RENDER_FULL_CONTENT}) async {
|
||||
Future<JCMediaDeviceVideoCanvas?> startRemoteVideo({
|
||||
int renderType = JCMediaDevice.RENDER_FULL_CONTENT,
|
||||
}) async {
|
||||
final item = await _call?.getActiveCallItem();
|
||||
if (item == null) return null;
|
||||
return item.startOtherVideo(renderType);
|
||||
@@ -162,10 +138,11 @@ class VideocallCallService with JCCallCallback {
|
||||
return item.stopOtherVideo();
|
||||
}
|
||||
|
||||
// -- Recording --
|
||||
|
||||
Future<bool> audioRecord(
|
||||
JCCallItem item, bool enable, String filePath) async {
|
||||
JCCallItem item,
|
||||
bool enable,
|
||||
String filePath,
|
||||
) async {
|
||||
if (_call == null) return false;
|
||||
return _call!.audioRecord(item, enable, filePath);
|
||||
}
|
||||
@@ -182,13 +159,18 @@ class VideocallCallService with JCCallCallback {
|
||||
}) async {
|
||||
if (_call == null) return false;
|
||||
return _call!.videoRecord(
|
||||
item, enable, remote, width, height, filePath, bothAudio, keyframe);
|
||||
item,
|
||||
enable,
|
||||
remote,
|
||||
width,
|
||||
height,
|
||||
filePath,
|
||||
bothAudio,
|
||||
keyframe,
|
||||
);
|
||||
}
|
||||
|
||||
// -- Messaging --
|
||||
|
||||
Future<bool> sendMessage(
|
||||
JCCallItem item, String type, String content) async {
|
||||
Future<bool> sendMessage(JCCallItem item, String type, String content) async {
|
||||
if (_call == null) return false;
|
||||
return _call!.sendMessage(item, type, content);
|
||||
}
|
||||
@@ -198,14 +180,9 @@ class VideocallCallService with JCCallCallback {
|
||||
return _call!.sendDtmf(item, value);
|
||||
}
|
||||
|
||||
// -- Items --
|
||||
|
||||
Future<List<JCCallItem>?> getCallItems() async => _call?.getCallItems();
|
||||
|
||||
Future<JCCallItem?> getActiveCallItem() async =>
|
||||
_call?.getActiveCallItem();
|
||||
|
||||
// -- Config --
|
||||
Future<JCCallItem?> getActiveCallItem() async => _call?.getActiveCallItem();
|
||||
|
||||
Future<String> getStatistics() async {
|
||||
if (_call == null) return '';
|
||||
@@ -232,20 +209,13 @@ class VideocallCallService with JCCallCallback {
|
||||
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;
|
||||
@@ -270,30 +240,36 @@ class VideocallCallService with JCCallCallback {
|
||||
);
|
||||
}
|
||||
|
||||
// -- JCCallCallback --
|
||||
|
||||
@override
|
||||
void onCallItemAdd(JCCallItem item) {
|
||||
debugPrint(
|
||||
'[VideocallSDK] onCallItemAdd: userId=${item.getUserId()}, video=${item.getVideo()}, direction=${item.getDirection()}',
|
||||
);
|
||||
_currentItem = _buildItem(item, VideocallState.pending);
|
||||
_callItemAddController.add(_currentItem!);
|
||||
}
|
||||
|
||||
@override
|
||||
void onCallItemUpdate(JCCallItem item, ChangeParam changeParam) {
|
||||
_currentItem = _buildItem(item, _mapCallState(item.getState()));
|
||||
final mappedState = _mapCallState(item.getState());
|
||||
debugPrint(
|
||||
'[VideocallSDK] onCallItemUpdate: rawState=${item.getState()}, mappedState=$mappedState, uploadSelf=${item.uploadVideoStreamSelf}, uploadOther=${item.uploadVideoStreamOther}',
|
||||
);
|
||||
_currentItem = _buildItem(item, mappedState);
|
||||
_callItemUpdateController.add(_currentItem!);
|
||||
}
|
||||
|
||||
@override
|
||||
void onCallItemRemove(JCCallItem item, int reason, String description) {
|
||||
debugPrint(
|
||||
'[VideocallSDK] onCallItemRemove: reason=$reason, description=$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));
|
||||
}
|
||||
void onMessageReceive(String type, String content, JCCallItem item) {}
|
||||
|
||||
@override
|
||||
void onMissedCallItem(JCCallItem item) {
|
||||
@@ -301,14 +277,10 @@ class VideocallCallService with JCCallCallback {
|
||||
}
|
||||
|
||||
@override
|
||||
void onDtmfReceived(JCCallItem item, int value) {
|
||||
_dtmfReceivedController.add((item: item, value: value));
|
||||
}
|
||||
void onDtmfReceived(JCCallItem item, int value) {}
|
||||
|
||||
@override
|
||||
void onEarlyMediaReceived(JCCallItem item) {
|
||||
_earlyMediaController.add(item);
|
||||
}
|
||||
void onEarlyMediaReceived(JCCallItem item) {}
|
||||
|
||||
@override
|
||||
void onSipRingInfoReceived(JCCallItem item, String callSipType) {}
|
||||
|
||||
@@ -17,7 +17,6 @@ class VideocallChannelService with JCMediaChannelCallback {
|
||||
JCMediaChannel? _channel;
|
||||
JCMediaChannel? get channel => _channel;
|
||||
|
||||
// -- Streams --
|
||||
|
||||
final _stateChangeController =
|
||||
StreamController<({int state, int oldState})>.broadcast();
|
||||
@@ -97,7 +96,6 @@ class VideocallChannelService with JCMediaChannelCallback {
|
||||
Stream<({int operationId, bool result, int reason})>
|
||||
get inviteSipUserResultStream => _inviteSipUserResultController.stream;
|
||||
|
||||
// -- Lifecycle --
|
||||
|
||||
Future<bool> initialize() async {
|
||||
final client = _clientRef.client;
|
||||
@@ -109,7 +107,6 @@ class VideocallChannelService with JCMediaChannelCallback {
|
||||
|
||||
Future<bool> destroy() async => JCMediaChannel.destroy();
|
||||
|
||||
// -- Channel actions --
|
||||
|
||||
Future<bool> join(String channelId, {JoinParam? joinParam}) async {
|
||||
if (_channel == null) return false;
|
||||
@@ -131,7 +128,6 @@ class VideocallChannelService with JCMediaChannelCallback {
|
||||
return _channel!.query(channelId);
|
||||
}
|
||||
|
||||
// -- Audio/Video streams --
|
||||
|
||||
Future<bool> enableUploadAudioStream(bool enable) async {
|
||||
if (_channel == null) return false;
|
||||
@@ -159,7 +155,6 @@ class VideocallChannelService with JCMediaChannelCallback {
|
||||
return _channel!.requestScreenVideo(screenUri, pictureSize);
|
||||
}
|
||||
|
||||
// -- Screen share / CDN / Recording --
|
||||
|
||||
Future<bool> enableScreenShare(
|
||||
bool enable, ScreenShareParam? screenShareParam) async {
|
||||
@@ -177,7 +172,6 @@ class VideocallChannelService with JCMediaChannelCallback {
|
||||
return _channel!.enableRecord(enable, recordParam);
|
||||
}
|
||||
|
||||
// -- Participants --
|
||||
|
||||
Future<List<JCMediaChannelParticipant>?> getParticipants() async =>
|
||||
_channel?.getParticipants();
|
||||
@@ -195,7 +189,6 @@ class VideocallChannelService with JCMediaChannelCallback {
|
||||
return _channel!.inviteSipUser(userId, sipParam);
|
||||
}
|
||||
|
||||
// -- Custom roles/state --
|
||||
|
||||
Future<bool> setCustomRole(int customRole,
|
||||
{JCMediaChannelParticipant? participant}) async {
|
||||
@@ -219,7 +212,6 @@ class VideocallChannelService with JCMediaChannelCallback {
|
||||
return _channel!.getCustomState();
|
||||
}
|
||||
|
||||
// -- Channel properties --
|
||||
|
||||
Future<int> setCustomProperty(String property) async {
|
||||
if (_channel == null) return -1;
|
||||
@@ -231,7 +223,6 @@ class VideocallChannelService with JCMediaChannelCallback {
|
||||
return _channel!.getCustomProperty();
|
||||
}
|
||||
|
||||
// -- Channel info --
|
||||
|
||||
Future<String> getChannelUri() async {
|
||||
if (_channel == null) return '';
|
||||
@@ -313,7 +304,6 @@ class VideocallChannelService with JCMediaChannelCallback {
|
||||
return _channel!.getScreenUserId();
|
||||
}
|
||||
|
||||
// -- Self participant --
|
||||
|
||||
Future<JCMediaChannelParticipant?> getSelfParticipant() async {
|
||||
return _channel?.getSelfParticipant();
|
||||
@@ -325,7 +315,6 @@ class VideocallChannelService with JCMediaChannelCallback {
|
||||
return _channel!.subscribeParticipantAudio(participant, subscribe);
|
||||
}
|
||||
|
||||
// -- Screen share video --
|
||||
|
||||
Future<JCMediaDeviceVideoCanvas?> startScreenShareVideo(
|
||||
int renderType, int pictureSize) async {
|
||||
@@ -337,7 +326,6 @@ class VideocallChannelService with JCMediaChannelCallback {
|
||||
return _channel!.stopScreenShareVideo();
|
||||
}
|
||||
|
||||
// -- Video ratio / resolution --
|
||||
|
||||
Future<bool> enableSelfVideoRatio(bool enable, double ratio) async {
|
||||
if (_channel == null) return false;
|
||||
@@ -349,7 +337,6 @@ class VideocallChannelService with JCMediaChannelCallback {
|
||||
return _channel!.getMaxResolution();
|
||||
}
|
||||
|
||||
// -- Messaging --
|
||||
|
||||
Future<bool> sendMessage(
|
||||
String type, String content, String toUserId) async {
|
||||
@@ -367,14 +354,12 @@ class VideocallChannelService with JCMediaChannelCallback {
|
||||
return _channel!.sendCommandToDelivery(command);
|
||||
}
|
||||
|
||||
// -- Statistics --
|
||||
|
||||
Future<String> getStatistics() async {
|
||||
if (_channel == null) return '';
|
||||
return _channel!.getStatistics();
|
||||
}
|
||||
|
||||
// -- Volume change notify --
|
||||
|
||||
Future<bool> enableVolumeChangeNotify(bool value) async {
|
||||
if (_channel == null) return false;
|
||||
@@ -386,7 +371,6 @@ class VideocallChannelService with JCMediaChannelCallback {
|
||||
return _channel!.getVolumeChangeNotify();
|
||||
}
|
||||
|
||||
// -- Dispose --
|
||||
|
||||
void dispose() {
|
||||
_stateChangeController.close();
|
||||
@@ -403,7 +387,6 @@ class VideocallChannelService with JCMediaChannelCallback {
|
||||
_inviteSipUserResultController.close();
|
||||
}
|
||||
|
||||
// -- JCMediaChannelCallback --
|
||||
|
||||
@override
|
||||
void onMediaChannelStateChange(int state, int oldState) =>
|
||||
|
||||
@@ -13,7 +13,6 @@ class VideocallClient with JCClientCallback {
|
||||
JCClient? _client;
|
||||
JCClient? get client => _client;
|
||||
|
||||
// -- Streams --
|
||||
|
||||
final _stateController = StreamController<VideocallClientState>.broadcast();
|
||||
Stream<VideocallClientState> get stateStream => _stateController.stream;
|
||||
@@ -46,7 +45,6 @@ class VideocallClient with JCClientCallback {
|
||||
VideocallClientState _state = VideocallClientState.notInitialized;
|
||||
VideocallClientState get state => _state;
|
||||
|
||||
// -- Lifecycle --
|
||||
|
||||
Future<bool> initialize() async {
|
||||
try {
|
||||
@@ -75,7 +73,6 @@ class VideocallClient with JCClientCallback {
|
||||
return result;
|
||||
}
|
||||
|
||||
// -- Auth --
|
||||
|
||||
Future<bool> login({
|
||||
required String userId,
|
||||
@@ -100,7 +97,6 @@ class VideocallClient with JCClientCallback {
|
||||
return _client!.logout();
|
||||
}
|
||||
|
||||
// -- User info --
|
||||
|
||||
Future<String?> getUserId() async => _client?.getUserId();
|
||||
|
||||
@@ -118,7 +114,6 @@ class VideocallClient with JCClientCallback {
|
||||
return _client!.getServerUid();
|
||||
}
|
||||
|
||||
// -- Server config --
|
||||
|
||||
Future<bool> setServerAddress(String serverAddress) async {
|
||||
if (_client == null) return false;
|
||||
@@ -130,14 +125,12 @@ class VideocallClient with JCClientCallback {
|
||||
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,
|
||||
@@ -147,7 +140,6 @@ class VideocallClient with JCClientCallback {
|
||||
return _client!.sendOnlineMessage(userId, content);
|
||||
}
|
||||
|
||||
// -- Params --
|
||||
|
||||
Future<CreateParam?> getCreateParam() async => _client?.getCreateParam();
|
||||
|
||||
@@ -158,7 +150,6 @@ class VideocallClient with JCClientCallback {
|
||||
return _client!.getState();
|
||||
}
|
||||
|
||||
// -- Dispose --
|
||||
|
||||
void dispose() {
|
||||
_stateController.close();
|
||||
@@ -169,7 +160,6 @@ class VideocallClient with JCClientCallback {
|
||||
_serverMessageController.close();
|
||||
}
|
||||
|
||||
// -- Internal --
|
||||
|
||||
void _updateState(VideocallClientState newState) {
|
||||
_state = newState;
|
||||
@@ -207,7 +197,6 @@ class VideocallClient with JCClientCallback {
|
||||
return LoginFailureReason.unknown;
|
||||
}
|
||||
|
||||
// -- JCClientCallback --
|
||||
|
||||
@override
|
||||
void onClientStateChange(int state, int oldState) {
|
||||
|
||||
@@ -12,7 +12,6 @@ class VideocallDeviceService with JCMediaDeviceCallback {
|
||||
JCMediaDevice? _mediaDevice;
|
||||
JCMediaDevice? get mediaDevice => _mediaDevice;
|
||||
|
||||
// -- Streams --
|
||||
|
||||
final _cameraUpdateController = StreamController<void>.broadcast();
|
||||
Stream<void> get cameraUpdateStream => _cameraUpdateController.stream;
|
||||
@@ -41,7 +40,6 @@ class VideocallDeviceService with JCMediaDeviceCallback {
|
||||
final _audioResumeController = StreamController<void>.broadcast();
|
||||
Stream<void> get audioResumeStream => _audioResumeController.stream;
|
||||
|
||||
// -- Lifecycle --
|
||||
|
||||
Future<bool> initialize() async {
|
||||
final client = _clientRef.client;
|
||||
@@ -52,7 +50,6 @@ class VideocallDeviceService with JCMediaDeviceCallback {
|
||||
|
||||
Future<bool> destroy() async => JCMediaDevice.destroy();
|
||||
|
||||
// -- Camera --
|
||||
|
||||
Future<bool> isCameraOpen() async {
|
||||
if (_mediaDevice == null) return false;
|
||||
@@ -106,7 +103,6 @@ class VideocallDeviceService with JCMediaDeviceCallback {
|
||||
return _mediaDevice!.getCameraType(cameraIndex);
|
||||
}
|
||||
|
||||
// -- Exposure --
|
||||
|
||||
Future<int> getMinExposureCompensation() async {
|
||||
if (_mediaDevice == null) return 0;
|
||||
@@ -133,7 +129,6 @@ class VideocallDeviceService with JCMediaDeviceCallback {
|
||||
return _mediaDevice!.setExposureCompensation(level);
|
||||
}
|
||||
|
||||
// -- Flash --
|
||||
|
||||
Future<bool> isCameraFlashSupported() async {
|
||||
if (_mediaDevice == null) return false;
|
||||
@@ -145,7 +140,6 @@ class VideocallDeviceService with JCMediaDeviceCallback {
|
||||
return _mediaDevice!.enableFlash(enable);
|
||||
}
|
||||
|
||||
// -- Focus/Zoom --
|
||||
|
||||
Future<bool> handleFocusMetering(
|
||||
JCMediaDeviceVideoCanvas canvas, double xPercent, double yPercent) async {
|
||||
@@ -168,7 +162,6 @@ class VideocallDeviceService with JCMediaDeviceCallback {
|
||||
return _mediaDevice!.getCameraCurrentZoom();
|
||||
}
|
||||
|
||||
// -- Speaker --
|
||||
|
||||
Future<bool> isSpeakerOn() async {
|
||||
if (_mediaDevice == null) return false;
|
||||
@@ -195,7 +188,6 @@ class VideocallDeviceService with JCMediaDeviceCallback {
|
||||
return _mediaDevice!.getAudioRouteType();
|
||||
}
|
||||
|
||||
// -- Audio --
|
||||
|
||||
Future<bool> isAudioStart() async {
|
||||
if (_mediaDevice == null) return false;
|
||||
@@ -245,7 +237,6 @@ class VideocallDeviceService with JCMediaDeviceCallback {
|
||||
Future<JCMediaDeviceAudioParam?> getAudioParam() async =>
|
||||
_mediaDevice?.getAudioParam();
|
||||
|
||||
// -- Volume --
|
||||
|
||||
Future<int> getOutputVolume() async {
|
||||
if (_mediaDevice == null) return 0;
|
||||
@@ -267,7 +258,6 @@ class VideocallDeviceService with JCMediaDeviceCallback {
|
||||
return _mediaDevice!.removeVolumeCallback(callback);
|
||||
}
|
||||
|
||||
// -- Video rendering --
|
||||
|
||||
Future<JCMediaDeviceVideoCanvas?> startCameraVideo(
|
||||
{int renderType = JCMediaDevice.RENDER_FULL_AUTO}) async {
|
||||
@@ -284,7 +274,6 @@ class VideocallDeviceService with JCMediaDeviceCallback {
|
||||
return _mediaDevice!.stopVideo(canvas);
|
||||
}
|
||||
|
||||
// -- Video file (custom capture) --
|
||||
|
||||
Future<bool> isVideoFileOpen() async {
|
||||
if (_mediaDevice == null) return false;
|
||||
@@ -313,7 +302,6 @@ class VideocallDeviceService with JCMediaDeviceCallback {
|
||||
return _mediaDevice!.stopVideoFile();
|
||||
}
|
||||
|
||||
// -- Video angle --
|
||||
|
||||
Future<bool> setVideoAngle(int angle) async {
|
||||
if (_mediaDevice == null) return false;
|
||||
@@ -325,7 +313,6 @@ class VideocallDeviceService with JCMediaDeviceCallback {
|
||||
return _mediaDevice!.getVideoAngle();
|
||||
}
|
||||
|
||||
// -- Frame callbacks --
|
||||
|
||||
Future<bool> setAudioFrameCallback(JCAudioFrameCallback? callback) async {
|
||||
if (_mediaDevice == null) return false;
|
||||
@@ -337,7 +324,6 @@ class VideocallDeviceService with JCMediaDeviceCallback {
|
||||
return _mediaDevice!.setVideoFrameCallback(callback);
|
||||
}
|
||||
|
||||
// -- Custom audio --
|
||||
|
||||
Future<bool> inputCustomAudioData(int sampleRateHz, int channels,
|
||||
Uint8List byteBuffer, int playDelayMS, int recDelayMS, int clockDrift) async {
|
||||
@@ -350,7 +336,6 @@ class VideocallDeviceService with JCMediaDeviceCallback {
|
||||
return _mediaDevice?.getAudioOutputData(sampleRateHz, channels);
|
||||
}
|
||||
|
||||
// -- Dispose --
|
||||
|
||||
void dispose() {
|
||||
_cameraUpdateController.close();
|
||||
@@ -362,7 +347,6 @@ class VideocallDeviceService with JCMediaDeviceCallback {
|
||||
_audioResumeController.close();
|
||||
}
|
||||
|
||||
// -- JCMediaDeviceCallback --
|
||||
|
||||
@override
|
||||
void onCameraUpdate() => _cameraUpdateController.add(null);
|
||||
|
||||
@@ -3,14 +3,12 @@ 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);
|
||||
@@ -20,20 +18,17 @@ class VideocallNetService with JCNetCallback {
|
||||
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) {
|
||||
|
||||
@@ -11,7 +11,6 @@ class VideocallPushService {
|
||||
JCPush? _push;
|
||||
JCPush? get push => _push;
|
||||
|
||||
// -- Lifecycle --
|
||||
|
||||
Future<bool> initialize() async {
|
||||
final client = _clientRef.client;
|
||||
@@ -25,7 +24,6 @@ class VideocallPushService {
|
||||
_push = null;
|
||||
}
|
||||
|
||||
// -- Push --
|
||||
|
||||
Future<bool> addPushInfo(JCPushTemplate info) async {
|
||||
if (_push == null) return false;
|
||||
@@ -37,7 +35,6 @@ class VideocallPushService {
|
||||
return _push!.addPushTemplate(data);
|
||||
}
|
||||
|
||||
// -- Dispose --
|
||||
|
||||
void dispose() {
|
||||
_push = null;
|
||||
|
||||
@@ -14,8 +14,11 @@ dependencies:
|
||||
jc_sdk: ^2.16.5
|
||||
flutter_riverpod: ^3.0.3
|
||||
get_it: ^9.0.5
|
||||
freezed_annotation: ^3.0.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_lints: ^5.0.0
|
||||
freezed: ^3.0.6
|
||||
build_runner: ^2.4.15
|
||||
|
||||
Reference in New Issue
Block a user