Skip to content

Commit

Permalink
Upgrade to video call implementation and dark mode
Browse files Browse the repository at this point in the history
  • Loading branch information
Mikael Wills committed Jul 29, 2024
1 parent 27d491f commit 4f2f118
Show file tree
Hide file tree
Showing 15 changed files with 681 additions and 223 deletions.
28 changes: 11 additions & 17 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import 'package:dart_sip_ua_example/src/theme_provider.dart';
import 'package:flutter/foundation.dart'
show debugDefaultTargetPlatformOverride;
import 'package:flutter/material.dart';
import 'package:logger/logger.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'package:provider/provider.dart';
import 'package:sip_ua/sip_ua.dart';

import 'src/about.dart';
Expand All @@ -10,10 +13,16 @@ import 'src/dialpad.dart';
import 'src/register.dart';

void main() {
Logger.level = Level.warning;
if (WebRTC.platformIsDesktop) {
debugDefaultTargetPlatformOverride = TargetPlatform.fuchsia;
}
runApp(MyApp());
runApp(
MultiProvider(
providers: [ChangeNotifierProvider(create: (_) => ThemeProvider())],
child: MyApp(),
),
);
}

typedef PageContentBuilder = Widget Function(
Expand Down Expand Up @@ -53,22 +62,7 @@ class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
fontFamily: 'Roboto',
inputDecorationTheme: InputDecorationTheme(
hintStyle: TextStyle(color: Colors.grey),
contentPadding: EdgeInsets.all(10.0),
border: UnderlineInputBorder(
borderSide: BorderSide(color: Colors.black12)),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.all(16),
textStyle: TextStyle(fontSize: 18),
),
),
),
theme: Provider.of<ThemeProvider>(context).currentTheme,
initialRoute: '/',
onGenerateRoute: _onGenerateRoute,
);
Expand Down
190 changes: 136 additions & 54 deletions example/lib/src/callscreen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,23 +28,22 @@ class _MyCallScreenWidget extends State<CallScreenWidget>
MediaStream? _remoteStream;

bool _showNumPad = false;
String _timeLabel = '00:00';
final ValueNotifier<String> _timeLabel = ValueNotifier<String>('00:00');
bool _audioMuted = false;
bool _videoMuted = false;
bool _speakerOn = false;
bool _hold = false;
bool _mirror = true;
String? _holdOriginator;
bool _callConfirmed = false;
CallStateEnum _state = CallStateEnum.NONE;

late String _transferTarget;
late Timer _timer;

SIPUAHelper? get helper => widget._helper;

bool get voiceOnly =>
(_localStream == null || _localStream!.getVideoTracks().isEmpty) &&
(_remoteStream == null || _remoteStream!.getVideoTracks().isEmpty);
bool get voiceOnly => call!.voiceOnly && !call!.remote_has_video;

String? get remoteIdentity => call!.remote_identity;

Expand All @@ -71,11 +70,9 @@ class _MyCallScreenWidget extends State<CallScreenWidget>
_timer = Timer.periodic(Duration(seconds: 1), (Timer timer) {
Duration duration = Duration(seconds: timer.tick);
if (mounted) {
setState(() {
_timeLabel = [duration.inMinutes, duration.inSeconds]
.map((seg) => seg.remainder(60).toString().padLeft(2, '0'))
.join(':');
});
_timeLabel.value = [duration.inMinutes, duration.inSeconds]
.map((seg) => seg.remainder(60).toString().padLeft(2, '0'))
.join(':');
} else {
_timer.cancel();
}
Expand Down Expand Up @@ -132,7 +129,7 @@ class _MyCallScreenWidget extends State<CallScreenWidget>

switch (callState.state) {
case CallStateEnum.STREAM:
_handelStreams(callState);
_handleStreams(callState);
break;
case CallStateEnum.ENDED:
case CallStateEnum.FAILED:
Expand All @@ -144,6 +141,8 @@ class _MyCallScreenWidget extends State<CallScreenWidget>
case CallStateEnum.PROGRESS:
case CallStateEnum.ACCEPTED:
case CallStateEnum.CONFIRMED:
setState(() => _callConfirmed = true);
break;
case CallStateEnum.HOLD:
case CallStateEnum.UNHOLD:
case CallStateEnum.NONE:
Expand Down Expand Up @@ -176,7 +175,7 @@ class _MyCallScreenWidget extends State<CallScreenWidget>
_cleanUp();
}

void _handelStreams(CallState event) async {
void _handleStreams(CallState event) async {
MediaStream? stream = event.stream;
if (event.originator == 'local') {
if (_localRenderer != null) {
Expand Down Expand Up @@ -221,18 +220,25 @@ class _MyCallScreenWidget extends State<CallScreenWidget>
final mediaConstraints = <String, dynamic>{
'audio': true,
'video': remoteHasVideo
? {
'mandatory': <String, dynamic>{
'minWidth': '640',
'minHeight': '480',
'minFrameRate': '30',
},
'facingMode': 'user',
}
: false
};
MediaStream mediaStream;

if (kIsWeb && remoteHasVideo) {
mediaStream =
await navigator.mediaDevices.getDisplayMedia(mediaConstraints);
mediaConstraints['video'] = false;
MediaStream userStream =
await navigator.mediaDevices.getUserMedia(mediaConstraints);
mediaStream.addTrack(userStream.getAudioTracks()[0], addToNative: true);
} else {
mediaConstraints['video'] = remoteHasVideo;
mediaStream = await navigator.mediaDevices.getUserMedia(mediaConstraints);
}

Expand Down Expand Up @@ -322,6 +328,19 @@ class _MyCallScreenWidget extends State<CallScreenWidget>
});
}

void _handleVideoUpgrade() {
if (voiceOnly) {
setState(() {
call!.voiceOnly = false;
});
helper!.renegotiate(
call: call!, voiceOnly: false, done: (incomingMessage) {});
} else {
helper!.renegotiate(
call: call!, voiceOnly: true, done: (incomingMessage) {});
}
}

void _toggleSpeaker() {
if (_localStream != null) {
_speakerOn = !_speakerOn;
Expand Down Expand Up @@ -388,6 +407,7 @@ class _MyCallScreenWidget extends State<CallScreenWidget>

final basicActions = <Widget>[];
final advanceActions = <Widget>[];
final advanceActions2 = <Widget>[];

switch (_state) {
case CallStateEnum.NONE:
Expand Down Expand Up @@ -435,6 +455,11 @@ class _MyCallScreenWidget extends State<CallScreenWidget>
checked: _speakerOn,
onPressed: () => _toggleSpeaker(),
));
advanceActions2.add(ActionButton(
title: 'request video',
icon: Icons.videocam,
onPressed: () => _handleVideoUpgrade(),
));
} else {
advanceActions.add(ActionButton(
title: _videoMuted ? "camera on" : 'camera off',
Expand Down Expand Up @@ -485,6 +510,16 @@ class _MyCallScreenWidget extends State<CallScreenWidget>
if (_showNumPad) {
actionWidgets.addAll(_buildNumPad());
} else {
if (advanceActions2.isNotEmpty) {
actionWidgets.add(
Padding(
padding: const EdgeInsets.all(3),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: advanceActions2),
),
);
}
if (advanceActions.isNotEmpty) {
actionWidgets.add(
Padding(
Expand Down Expand Up @@ -514,6 +549,7 @@ class _MyCallScreenWidget extends State<CallScreenWidget>
}

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

if (!voiceOnly && _remoteStream != null) {
Expand Down Expand Up @@ -543,54 +579,60 @@ class _MyCallScreenWidget extends State<CallScreenWidget>
),
);
}

stackWidgets.addAll(
[
Positioned(
top: voiceOnly ? 48 : 6,
left: 0,
right: 0,
child: Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Center(
child: Padding(
padding: const EdgeInsets.all(6),
child: Text(
(voiceOnly ? 'VOICE CALL' : 'VIDEO CALL') +
(_hold
? ' PAUSED BY ${_holdOriginator!.toUpperCase()}'
: ''),
style: TextStyle(fontSize: 24, color: Colors.black54),
if (voiceOnly || !_callConfirmed) {
stackWidgets.addAll(
[
Positioned(
top: MediaQuery.of(context).size.height / 8,
left: 0,
right: 0,
child: Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Center(
child: Padding(
padding: const EdgeInsets.all(6),
child: Text(
(voiceOnly ? 'VOICE CALL' : 'VIDEO CALL') +
(_hold
? ' PAUSED BY ${_holdOriginator!.toUpperCase()}'
: ''),
style: TextStyle(fontSize: 24, color: textColor),
),
),
),
),
Center(
child: Padding(
padding: const EdgeInsets.all(6),
child: Text(
'$remoteIdentity',
style: TextStyle(fontSize: 18, color: Colors.black54),
Center(
child: Padding(
padding: const EdgeInsets.all(6),
child: Text(
'$remoteIdentity',
style: TextStyle(fontSize: 18, color: textColor),
),
),
),
),
Center(
child: Padding(
padding: const EdgeInsets.all(6),
child: Text(
_timeLabel,
style: TextStyle(fontSize: 14, color: Colors.black54),
Center(
child: Padding(
padding: const EdgeInsets.all(6),
child: ValueListenableBuilder<String>(
valueListenable: _timeLabel,
builder: (context, value, child) {
return Text(
_timeLabel.value,
style: TextStyle(fontSize: 14, color: textColor),
);
},
),
),
),
)
],
)
],
),
),
),
),
],
);
],
);
}

return Stack(
children: stackWidgets,
Expand All @@ -614,6 +656,46 @@ class _MyCallScreenWidget extends State<CallScreenWidget>
);
}

@override
void onNewReinvite(ReInvite event) {
if (event.accept == null) return;
if (event.reject == null) return;
if (voiceOnly && (event.hasVideo ?? false)) {
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return AlertDialog(
title: Text('Upgrade to video?'),
content: Text('$remoteIdentity is inviting you to video call'),
alignment: Alignment.center,
actionsAlignment: MainAxisAlignment.spaceBetween,
actions: <Widget>[
TextButton(
child: const Text('Cancel'),
onPressed: () {
event.reject!.call({'status_code': 607});
Navigator.of(context).pop();
},
),
TextButton(
child: const Text('OK'),
onPressed: () {
event.accept!.call({});
setState(() {
call!.voiceOnly = false;
_resizeLocalVideo();
});
Navigator.of(context).pop();
},
),
],
);
},
);
}
}

@override
void onNewMessage(SIPMessageRequest msg) {
// NO OP
Expand Down
Loading

0 comments on commit 4f2f118

Please sign in to comment.