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