Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
202 changes: 111 additions & 91 deletions lib/view/screen/home_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -117,11 +117,18 @@ class _HomeScreenState extends State<HomeScreen> {
case 'Disable Debugging':
Provider.of<ProfileProvider>(context, listen: false).toggleDebugMode();
break;
case 'Enable Trickle ICE':
case 'Disable Trickle ICE':
Provider.of<TelnyxClientViewModel>(context, listen: false)
.toggleTrickleIce();
break;
case 'Assistant Login':
_showAssistantLoginDialog();
break;
case 'Force ICE Renegotiation':
Provider.of<TelnyxClientViewModel>(context, listen: false).forceIceRenegotiation();
Provider
.of<TelnyxClientViewModel>(context, listen: false)
.forceIceRenegotiation();
break;
}
}
Expand All @@ -138,33 +145,37 @@ class _HomeScreenState extends State<HomeScreen> {
@override
Widget build(BuildContext context) {
final clientState = context.select<TelnyxClientViewModel, CallStateStatus>(
(txClient) => txClient.callState,
(txClient) => txClient.callState,
);

final profileProvider = context.watch<ProfileProvider>();
final selectedProfile = profileProvider.selectedProfile;

final clientViewModel = context.watch<TelnyxClientViewModel>();
final useTrickleIce = clientViewModel.useTrickleIce;

final errorMessage = context.select<TelnyxClientViewModel, String?>(
(viewModel) => viewModel.errorDialogMessage,
(viewModel) => viewModel.errorDialogMessage,
);

if (errorMessage != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
showDialog(
context: context,
builder: (_) => AlertDialog(
title: const Text('Error'),
content: Text(errorMessage),
actions: [
TextButton(
onPressed: () {
context.read<TelnyxClientViewModel>().clearErrorDialog();
Navigator.of(context).pop();
},
child: const Text('OK'),
builder: (_) =>
AlertDialog(
title: const Text('Error'),
content: Text(errorMessage),
actions: [
TextButton(
onPressed: () {
context.read<TelnyxClientViewModel>().clearErrorDialog();
Navigator.of(context).pop();
},
child: const Text('OK'),
),
],
),
],
),
);
});
}
Expand All @@ -177,56 +188,64 @@ class _HomeScreenState extends State<HomeScreen> {
PopupMenuButton<String>(
onSelected: handleOptionClick,
itemBuilder: (BuildContext context) {
final trickleIceToggleText = useTrickleIce
? 'Disable Trickle ICE'
: 'Enable Trickle ICE';
return {
'Audio Codecs',
'Export Logs',
'Disable Push Notifications',
'Force ICE Renegotiation'
}.map((
String choice,
) {
return PopupMenuItem<String>(
value: choice,
child: Text(choice),
);
}).toList();
},
)
else if (clientState == CallStateStatus.disconnected &&
selectedProfile != null)
PopupMenuButton<String>(
onSelected: handleOptionClick,
itemBuilder: (BuildContext context) {
final debugToggleText = selectedProfile.isDebug
? 'Disable Debugging'
: 'Enable Debugging';
return {'Export Logs', debugToggleText, 'Assistant Login', 'Force ICE Renegotiation'}
.map((String choice) {
return PopupMenuItem<String>(
value: choice,
child: Text(choice),
);
}).toList();
},
)
else if (clientState == CallStateStatus.ongoingCall ||
clientState == CallStateStatus.ringing ||
clientState == CallStateStatus.ongoingInvitation ||
clientState == CallStateStatus.connectingToCall)
PopupMenuButton<String>(
onSelected: handleOptionClick,
itemBuilder: (BuildContext context) {
return {
'Force ICE Renegotiation',
'Audio Codecs',
'Export Logs',
}.map((String choice) {
'Disable Push Notifications',
'Force ICE Renegotiation'
}.map((String choice,) {
return PopupMenuItem<String>(
value: choice,
child: Text(choice),
);
}).toList();
},
),
)
else
if (clientState == CallStateStatus.disconnected &&
selectedProfile != null)
PopupMenuButton<String>(
onSelected: handleOptionClick,
itemBuilder: (BuildContext context) {
final debugToggleText = selectedProfile.isDebug
? 'Disable Debugging'
: 'Enable Debugging';
return {
'Export Logs',
debugToggleText,
'Assistant Login',
'Force ICE Renegotiation'
}
.map((String choice) {
return PopupMenuItem<String>(
value: choice,
child: Text(choice),
);
}).toList();
},
)
else
if (clientState == CallStateStatus.ongoingCall ||
clientState == CallStateStatus.ringing ||
clientState == CallStateStatus.ongoingInvitation ||
clientState == CallStateStatus.connectingToCall)
PopupMenuButton<String>(
onSelected: handleOptionClick,
itemBuilder: (BuildContext context) {
return {
'Force ICE Renegotiation',
'Export Logs',
}.map((String choice) {
return PopupMenuItem<String>(
value: choice,
child: Text(choice),
);
}).toList();
},
),
],
),
body: SingleChildScrollView(
Expand All @@ -249,41 +268,42 @@ class _HomeScreenState extends State<HomeScreen> {
),
bottomNavigationBar: clientState == CallStateStatus.idle
? Padding(
padding: const EdgeInsets.all(spacingXXL),
child: BottomConnectionActionWidget(
buttonTitle: 'Disconnect',
onPressed: () => {
context.read<TelnyxClientViewModel>().disconnect(),
},
),
)
padding: const EdgeInsets.all(spacingXXL),
child: BottomConnectionActionWidget(
buttonTitle: 'Disconnect',
onPressed: () =>
{
context.read<TelnyxClientViewModel>().disconnect(),
},
),
)
: clientState == CallStateStatus.disconnected
? // Connect Bottom Action widget positioned at the bottom
Consumer<TelnyxClientViewModel>(
builder: (context, viewModel, child) {
final profileProvider = context.watch<ProfileProvider>();
final selectedProfile = profileProvider.selectedProfile;
return Padding(
padding: const EdgeInsets.all(spacingXXL),
child: BottomConnectionActionWidget(
buttonTitle: 'Connect',
isLoading: viewModel.loggingIn,
onPressed: selectedProfile != null
? () async {
final config =
await selectedProfile.toTelnyxConfig();
if (config is TokenConfig) {
viewModel.loginWithToken(config);
} else if (config is CredentialConfig) {
viewModel.login(config);
}
}
: null,
),
);
},
)
: null,
? // Connect Bottom Action widget positioned at the bottom
Consumer<TelnyxClientViewModel>(
builder: (context, viewModel, child) {
final profileProvider = context.watch<ProfileProvider>();
final selectedProfile = profileProvider.selectedProfile;
return Padding(
padding: const EdgeInsets.all(spacingXXL),
child: BottomConnectionActionWidget(
buttonTitle: 'Connect',
isLoading: viewModel.loggingIn,
onPressed: selectedProfile != null
? () async {
final config =
await selectedProfile.toTelnyxConfig();
if (config is TokenConfig) {
viewModel.loginWithToken(config);
} else if (config is CredentialConfig) {
viewModel.login(config);
}
}
: null,
),
);
},
)
: null,
);
}
}
13 changes: 13 additions & 0 deletions lib/view/telnyx_client_view_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ class TelnyxClientViewModel with ChangeNotifier {
bool _mute = false;
bool _hold = false;
bool _isAssistantMode = false;
bool _useTrickleIce = false;
List<AudioCodec> _supportedCodecs = [];
List<AudioCodec> _preferredCodecs = [];

Expand Down Expand Up @@ -128,6 +129,13 @@ class TelnyxClientViewModel with ChangeNotifier {
return _isAssistantMode;
}

bool get useTrickleIce => _useTrickleIce;

void toggleTrickleIce() {
_useTrickleIce = !_useTrickleIce;
notifyListeners();
}

List<AudioCodec> get supportedCodecs => _supportedCodecs;

List<AudioCodec> get preferredCodecs => _preferredCodecs;
Expand Down Expand Up @@ -388,6 +396,7 @@ class TelnyxClientViewModel with ChangeNotifier {
if (config.notificationToken != null) {
await prefs.setString('notificationToken', config.notificationToken!);
}
await prefs.setBool('forceRelayCandidate', config.forceRelayCandidate);
}

Future<void> _clearConfigForAutoLogin() async {
Expand All @@ -398,6 +407,8 @@ class TelnyxClientViewModel with ChangeNotifier {
await prefs.remove('sipName');
await prefs.remove('sipNumber');
await prefs.remove('notificationToken');
await prefs.remove('forceRelayCandidate');
await prefs.remove('useTrickleIce');
}

void observeResponses() {
Expand Down Expand Up @@ -791,6 +802,7 @@ class TelnyxClientViewModel with ChangeNotifier {
customHeaders: {'X-Header-1': 'Value1', 'X-Header-2': 'Value2'},
preferredCodecs: _preferredCodecs.isNotEmpty ? _preferredCodecs : null,
debug: true,
useTrickleIce: _useTrickleIce,
);

logger.i(
Expand Down Expand Up @@ -907,6 +919,7 @@ class TelnyxClientViewModel with ChangeNotifier {
'State',
customHeaders: {},
debug: true,
useTrickleIce: _useTrickleIce,
);
observeCurrentCall();

Expand Down
8 changes: 6 additions & 2 deletions packages/telnyx_webrtc/lib/call.dart
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,9 @@ class Call {
/// - Represents states like: newCall, ringing, connecting, active, held, done, etc.
late CallState callState;

/// AudioService instance to handle audio playback
final audioService = AudioService();
/// AudioService instance to handle audio playback (lazy initialized)
AudioService get audioService => _audioService ??= AudioService();
AudioService? _audioService;

/// Debug mode flag to enable call quality metrics
final bool debug;
Expand Down Expand Up @@ -249,6 +250,7 @@ class Call {
/// @param isAttach Whether this is an attach operation
/// @param customHeaders Optional custom SIP headers
/// @param debug Whether to enable call quality metrics (default: false)
/// @param useTrickleIce Whether to use trickle ICE for faster call setup (default: false)
Call acceptCall(
IncomingInviteParams invite,
String callerName,
Expand All @@ -257,6 +259,7 @@ class Call {
bool isAttach = false,
Map<String, String> customHeaders = const {},
bool debug = false,
bool useTrickleIce = false,
}) {
// Store the session information for later use
sessionCallerName = callerName;
Expand All @@ -276,6 +279,7 @@ class Call {
customHeaders: customHeaders,
isAttach: isAttach,
debug: debug,
useTrickleIce: useTrickleIce,
);
}

Expand Down
31 changes: 31 additions & 0 deletions packages/telnyx_webrtc/lib/model/pending_ice_candidate.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/// Represents a pending ICE candidate that needs to be processed
/// after the remote description is set.
class PendingIceCandidate {
/// The call ID this candidate belongs to
final String callId;

/// The SDP media identifier
final String sdpMid;

/// The SDP media line index
final int sdpMLineIndex;

/// The original candidate string
final String candidateString;

/// The enhanced candidate string (will be set later when ICE parameters are available)
final String enhancedCandidateString;

PendingIceCandidate({
required this.callId,
required this.sdpMid,
required this.sdpMLineIndex,
required this.candidateString,
required this.enhancedCandidateString,
});

@override
String toString() {
return 'PendingIceCandidate(callId: $callId, sdpMid: $sdpMid, sdpMLineIndex: $sdpMLineIndex, candidate: $candidateString)';
}
}
2 changes: 2 additions & 0 deletions packages/telnyx_webrtc/lib/model/socket_method.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ class SocketMethod {
static const attach = 'telnyx_rtc.attach';
static const disablePush = 'telnyx_rtc.disable_push_notification';
static const aiConversation = 'ai_conversation';
static const candidate = 'telnyx_rtc.candidate';
static const endOfCandidates = 'telnyx_rtc.endOfCandidates';
}
Loading
Loading