Skip to content

Commit 02ecf80

Browse files
mikaelwillsMikael Wills
andauthored
Upgrade to video call implementation and dark mode (#462)
Co-authored-by: Mikael Wills <[email protected]>
1 parent c01e084 commit 02ecf80

File tree

15 files changed

+681
-224
lines changed

15 files changed

+681
-224
lines changed

example/lib/main.dart

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
import 'package:dart_sip_ua_example/src/theme_provider.dart';
12
import 'package:flutter/foundation.dart'
23
show debugDefaultTargetPlatformOverride;
34
import 'package:flutter/material.dart';
5+
import 'package:logger/logger.dart';
46
import 'package:flutter_webrtc/flutter_webrtc.dart';
7+
import 'package:provider/provider.dart';
58
import 'package:sip_ua/sip_ua.dart';
69

710
import 'src/about.dart';
@@ -10,10 +13,16 @@ import 'src/dialpad.dart';
1013
import 'src/register.dart';
1114

1215
void main() {
16+
Logger.level = Level.warning;
1317
if (WebRTC.platformIsDesktop) {
1418
debugDefaultTargetPlatformOverride = TargetPlatform.fuchsia;
1519
}
16-
runApp(MyApp());
20+
runApp(
21+
MultiProvider(
22+
providers: [ChangeNotifierProvider(create: (_) => ThemeProvider())],
23+
child: MyApp(),
24+
),
25+
);
1726
}
1827

1928
typedef PageContentBuilder = Widget Function(
@@ -53,22 +62,7 @@ class MyApp extends StatelessWidget {
5362
Widget build(BuildContext context) {
5463
return MaterialApp(
5564
title: 'Flutter Demo',
56-
theme: ThemeData(
57-
primarySwatch: Colors.blue,
58-
fontFamily: 'Roboto',
59-
inputDecorationTheme: InputDecorationTheme(
60-
hintStyle: TextStyle(color: Colors.grey),
61-
contentPadding: EdgeInsets.all(10.0),
62-
border: UnderlineInputBorder(
63-
borderSide: BorderSide(color: Colors.black12)),
64-
),
65-
elevatedButtonTheme: ElevatedButtonThemeData(
66-
style: ElevatedButton.styleFrom(
67-
padding: const EdgeInsets.all(16),
68-
textStyle: TextStyle(fontSize: 18),
69-
),
70-
),
71-
),
65+
theme: Provider.of<ThemeProvider>(context).currentTheme,
7266
initialRoute: '/',
7367
onGenerateRoute: _onGenerateRoute,
7468
);

example/lib/src/callscreen.dart

Lines changed: 136 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -28,23 +28,22 @@ class _MyCallScreenWidget extends State<CallScreenWidget>
2828
MediaStream? _remoteStream;
2929

3030
bool _showNumPad = false;
31-
String _timeLabel = '00:00';
31+
final ValueNotifier<String> _timeLabel = ValueNotifier<String>('00:00');
3232
bool _audioMuted = false;
3333
bool _videoMuted = false;
3434
bool _speakerOn = false;
3535
bool _hold = false;
3636
bool _mirror = true;
3737
String? _holdOriginator;
38+
bool _callConfirmed = false;
3839
CallStateEnum _state = CallStateEnum.NONE;
3940

4041
late String _transferTarget;
4142
late Timer _timer;
4243

4344
SIPUAHelper? get helper => widget._helper;
4445

45-
bool get voiceOnly =>
46-
(_localStream == null || _localStream!.getVideoTracks().isEmpty) &&
47-
(_remoteStream == null || _remoteStream!.getVideoTracks().isEmpty);
46+
bool get voiceOnly => call!.voiceOnly && !call!.remote_has_video;
4847

4948
String? get remoteIdentity => call!.remote_identity;
5049

@@ -71,11 +70,9 @@ class _MyCallScreenWidget extends State<CallScreenWidget>
7170
_timer = Timer.periodic(Duration(seconds: 1), (Timer timer) {
7271
Duration duration = Duration(seconds: timer.tick);
7372
if (mounted) {
74-
setState(() {
75-
_timeLabel = [duration.inMinutes, duration.inSeconds]
76-
.map((seg) => seg.remainder(60).toString().padLeft(2, '0'))
77-
.join(':');
78-
});
73+
_timeLabel.value = [duration.inMinutes, duration.inSeconds]
74+
.map((seg) => seg.remainder(60).toString().padLeft(2, '0'))
75+
.join(':');
7976
} else {
8077
_timer.cancel();
8178
}
@@ -132,7 +129,7 @@ class _MyCallScreenWidget extends State<CallScreenWidget>
132129

133130
switch (callState.state) {
134131
case CallStateEnum.STREAM:
135-
_handelStreams(callState);
132+
_handleStreams(callState);
136133
break;
137134
case CallStateEnum.ENDED:
138135
case CallStateEnum.FAILED:
@@ -144,6 +141,8 @@ class _MyCallScreenWidget extends State<CallScreenWidget>
144141
case CallStateEnum.PROGRESS:
145142
case CallStateEnum.ACCEPTED:
146143
case CallStateEnum.CONFIRMED:
144+
setState(() => _callConfirmed = true);
145+
break;
147146
case CallStateEnum.HOLD:
148147
case CallStateEnum.UNHOLD:
149148
case CallStateEnum.NONE:
@@ -176,7 +175,7 @@ class _MyCallScreenWidget extends State<CallScreenWidget>
176175
_cleanUp();
177176
}
178177

179-
void _handelStreams(CallState event) async {
178+
void _handleStreams(CallState event) async {
180179
MediaStream? stream = event.stream;
181180
if (event.originator == 'local') {
182181
if (_localRenderer != null) {
@@ -221,18 +220,25 @@ class _MyCallScreenWidget extends State<CallScreenWidget>
221220
final mediaConstraints = <String, dynamic>{
222221
'audio': true,
223222
'video': remoteHasVideo
223+
? {
224+
'mandatory': <String, dynamic>{
225+
'minWidth': '640',
226+
'minHeight': '480',
227+
'minFrameRate': '30',
228+
},
229+
'facingMode': 'user',
230+
}
231+
: false
224232
};
225233
MediaStream mediaStream;
226234

227235
if (kIsWeb && remoteHasVideo) {
228236
mediaStream =
229237
await navigator.mediaDevices.getDisplayMedia(mediaConstraints);
230-
mediaConstraints['video'] = false;
231238
MediaStream userStream =
232239
await navigator.mediaDevices.getUserMedia(mediaConstraints);
233240
mediaStream.addTrack(userStream.getAudioTracks()[0], addToNative: true);
234241
} else {
235-
mediaConstraints['video'] = remoteHasVideo;
236242
mediaStream = await navigator.mediaDevices.getUserMedia(mediaConstraints);
237243
}
238244

@@ -322,6 +328,19 @@ class _MyCallScreenWidget extends State<CallScreenWidget>
322328
});
323329
}
324330

331+
void _handleVideoUpgrade() {
332+
if (voiceOnly) {
333+
setState(() {
334+
call!.voiceOnly = false;
335+
});
336+
helper!.renegotiate(
337+
call: call!, voiceOnly: false, done: (incomingMessage) {});
338+
} else {
339+
helper!.renegotiate(
340+
call: call!, voiceOnly: true, done: (incomingMessage) {});
341+
}
342+
}
343+
325344
void _toggleSpeaker() {
326345
if (_localStream != null) {
327346
_speakerOn = !_speakerOn;
@@ -388,6 +407,7 @@ class _MyCallScreenWidget extends State<CallScreenWidget>
388407

389408
final basicActions = <Widget>[];
390409
final advanceActions = <Widget>[];
410+
final advanceActions2 = <Widget>[];
391411

392412
switch (_state) {
393413
case CallStateEnum.NONE:
@@ -435,6 +455,11 @@ class _MyCallScreenWidget extends State<CallScreenWidget>
435455
checked: _speakerOn,
436456
onPressed: () => _toggleSpeaker(),
437457
));
458+
advanceActions2.add(ActionButton(
459+
title: 'request video',
460+
icon: Icons.videocam,
461+
onPressed: () => _handleVideoUpgrade(),
462+
));
438463
} else {
439464
advanceActions.add(ActionButton(
440465
title: _videoMuted ? "camera on" : 'camera off',
@@ -485,6 +510,16 @@ class _MyCallScreenWidget extends State<CallScreenWidget>
485510
if (_showNumPad) {
486511
actionWidgets.addAll(_buildNumPad());
487512
} else {
513+
if (advanceActions2.isNotEmpty) {
514+
actionWidgets.add(
515+
Padding(
516+
padding: const EdgeInsets.all(3),
517+
child: Row(
518+
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
519+
children: advanceActions2),
520+
),
521+
);
522+
}
488523
if (advanceActions.isNotEmpty) {
489524
actionWidgets.add(
490525
Padding(
@@ -514,6 +549,7 @@ class _MyCallScreenWidget extends State<CallScreenWidget>
514549
}
515550

516551
Widget _buildContent() {
552+
Color? textColor = Theme.of(context).textTheme.bodyMedium?.color;
517553
final stackWidgets = <Widget>[];
518554

519555
if (!voiceOnly && _remoteStream != null) {
@@ -543,54 +579,60 @@ class _MyCallScreenWidget extends State<CallScreenWidget>
543579
),
544580
);
545581
}
546-
547-
stackWidgets.addAll(
548-
[
549-
Positioned(
550-
top: voiceOnly ? 48 : 6,
551-
left: 0,
552-
right: 0,
553-
child: Center(
554-
child: Column(
555-
crossAxisAlignment: CrossAxisAlignment.center,
556-
mainAxisAlignment: MainAxisAlignment.center,
557-
children: <Widget>[
558-
Center(
559-
child: Padding(
560-
padding: const EdgeInsets.all(6),
561-
child: Text(
562-
(voiceOnly ? 'VOICE CALL' : 'VIDEO CALL') +
563-
(_hold
564-
? ' PAUSED BY ${_holdOriginator!.toUpperCase()}'
565-
: ''),
566-
style: TextStyle(fontSize: 24, color: Colors.black54),
582+
if (voiceOnly || !_callConfirmed) {
583+
stackWidgets.addAll(
584+
[
585+
Positioned(
586+
top: MediaQuery.of(context).size.height / 8,
587+
left: 0,
588+
right: 0,
589+
child: Center(
590+
child: Column(
591+
crossAxisAlignment: CrossAxisAlignment.center,
592+
mainAxisAlignment: MainAxisAlignment.center,
593+
children: <Widget>[
594+
Center(
595+
child: Padding(
596+
padding: const EdgeInsets.all(6),
597+
child: Text(
598+
(voiceOnly ? 'VOICE CALL' : 'VIDEO CALL') +
599+
(_hold
600+
? ' PAUSED BY ${_holdOriginator!.toUpperCase()}'
601+
: ''),
602+
style: TextStyle(fontSize: 24, color: textColor),
603+
),
567604
),
568605
),
569-
),
570-
Center(
571-
child: Padding(
572-
padding: const EdgeInsets.all(6),
573-
child: Text(
574-
'$remoteIdentity',
575-
style: TextStyle(fontSize: 18, color: Colors.black54),
606+
Center(
607+
child: Padding(
608+
padding: const EdgeInsets.all(6),
609+
child: Text(
610+
'$remoteIdentity',
611+
style: TextStyle(fontSize: 18, color: textColor),
612+
),
576613
),
577614
),
578-
),
579-
Center(
580-
child: Padding(
581-
padding: const EdgeInsets.all(6),
582-
child: Text(
583-
_timeLabel,
584-
style: TextStyle(fontSize: 14, color: Colors.black54),
615+
Center(
616+
child: Padding(
617+
padding: const EdgeInsets.all(6),
618+
child: ValueListenableBuilder<String>(
619+
valueListenable: _timeLabel,
620+
builder: (context, value, child) {
621+
return Text(
622+
_timeLabel.value,
623+
style: TextStyle(fontSize: 14, color: textColor),
624+
);
625+
},
626+
),
585627
),
586-
),
587-
)
588-
],
628+
)
629+
],
630+
),
589631
),
590632
),
591-
),
592-
],
593-
);
633+
],
634+
);
635+
}
594636

595637
return Stack(
596638
children: stackWidgets,
@@ -614,6 +656,46 @@ class _MyCallScreenWidget extends State<CallScreenWidget>
614656
);
615657
}
616658

659+
@override
660+
void onNewReinvite(ReInvite event) {
661+
if (event.accept == null) return;
662+
if (event.reject == null) return;
663+
if (voiceOnly && (event.hasVideo ?? false)) {
664+
showDialog(
665+
context: context,
666+
barrierDismissible: false,
667+
builder: (BuildContext context) {
668+
return AlertDialog(
669+
title: Text('Upgrade to video?'),
670+
content: Text('$remoteIdentity is inviting you to video call'),
671+
alignment: Alignment.center,
672+
actionsAlignment: MainAxisAlignment.spaceBetween,
673+
actions: <Widget>[
674+
TextButton(
675+
child: const Text('Cancel'),
676+
onPressed: () {
677+
event.reject!.call({'status_code': 607});
678+
Navigator.of(context).pop();
679+
},
680+
),
681+
TextButton(
682+
child: const Text('OK'),
683+
onPressed: () {
684+
event.accept!.call({});
685+
setState(() {
686+
call!.voiceOnly = false;
687+
_resizeLocalVideo();
688+
});
689+
Navigator.of(context).pop();
690+
},
691+
),
692+
],
693+
);
694+
},
695+
);
696+
}
697+
}
698+
617699
@override
618700
void onNewMessage(SIPMessageRequest msg) {
619701
// NO OP

0 commit comments

Comments
 (0)