* build: Reenable shrink resources and minify in gradle * build: (deps): bump image from 4.6.0 to 4.7.1 Bumps [image](https://github.com/brendan-duncan/image) from 4.6.0 to 4.7.1. - [Changelog](https://github.com/brendan-duncan/image/blob/main/CHANGELOG.md) - [Commits](https://github.com/brendan-duncan/image/commits) --- updated-dependencies: - dependency-name: image dependency-version: 4.7.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> * build: (deps): bump file_picker from 10.3.7 to 10.3.8 Bumps [file_picker](https://github.com/miguelpruivo/flutter_file_picker) from 10.3.7 to 10.3.8. - [Release notes](https://github.com/miguelpruivo/flutter_file_picker/releases) - [Changelog](https://github.com/miguelpruivo/flutter_file_picker/blob/master/CHANGELOG.md) - [Commits](https://github.com/miguelpruivo/flutter_file_picker/compare/v10.3.7...v10.3.8) --- updated-dependencies: - dependency-name: file_picker dependency-version: 10.3.8 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> * feat: Improved search * build: Use matrix sdk vom pub.dev again * chore: Follow up better search * build: (deps): bump image from 4.7.1 to 4.7.2 Bumps [image](https://github.com/brendan-duncan/image) from 4.7.1 to 4.7.2. - [Changelog](https://github.com/brendan-duncan/image/blob/main/CHANGELOG.md) - [Commits](https://github.com/brendan-duncan/image/commits) --- updated-dependencies: - dependency-name: image dependency-version: 4.7.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> * chore: Make cross signing self sign mandatory for bootstrap * chore: Update user device keys before creating bootstrap * fix: Better wait for secrets after verification bootstrap * refactor: Remove native imaging and enable web worker * refactor: Remove unused html onfocus streams * build: (deps): bump flutter_foreground_task from 9.1.0 to 9.2.0 Bumps [flutter_foreground_task](https://github.com/Dev-hwang/flutter_foreground_task) from 9.1.0 to 9.2.0. - [Changelog](https://github.com/Dev-hwang/flutter_foreground_task/blob/master/CHANGELOG.md) - [Commits](https://github.com/Dev-hwang/flutter_foreground_task/commits) --- updated-dependencies: - dependency-name: flutter_foreground_task dependency-version: 9.2.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> * chore(translations): Translated using Weblate (Uzbek) Currently translated at 99.7% (823 of 825 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/uz/ * chore(translations): Translated using Weblate (Russian) Currently translated at 99.8% (824 of 825 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/ru/ * chore(translations): Translated using Weblate (Norwegian Bokmål) Currently translated at 90.9% (750 of 825 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/nb_NO/ * chore(translations): Translated using Weblate (Galician) Currently translated at 100.0% (825 of 825 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/gl/ * chore(translations): Translated using Weblate (Basque) Currently translated at 99.7% (823 of 825 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/eu/ * chore(translations): Translated using Weblate (Ukrainian) Currently translated at 100.0% (825 of 825 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/uk/ * chore(translations): Translated using Weblate (Estonian) Currently translated at 100.0% (825 of 825 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/et/ * chore(translations): Translated using Weblate (Dutch) Currently translated at 100.0% (825 of 825 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/nl/ * chore(translations): Translated using Weblate (Russian) Currently translated at 100.0% (825 of 825 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/ru/ * chore(translations): Translated using Weblate (Spanish) Currently translated at 95.2% (788 of 827 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/es/ * chore(translations): Translated using Weblate (Spanish) Currently translated at 96.3% (797 of 827 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/es/ * chore(translations): Translated using Weblate (Russian) Currently translated at 100.0% (825 of 825 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/ru/ * chore(translations): Translated using Weblate (Russian) Currently translated at 100.0% (825 of 825 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/ru/ * fix: Broken ruzzian plurals * chore(translations): Translated using Weblate (Norwegian Bokmål) Currently translated at 91.2% (753 of 825 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/nb_NO/ * chore(translations): Translated using Weblate (Bengali) Currently translated at 4.5% (38 of 827 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/bn/ * chore(translations): Translated using Weblate (French) Currently translated at 82.3% (679 of 825 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/fr/ * build: (deps): bump translations_cleaner from 0.0.5 to 0.1.0 Bumps [translations_cleaner](https://github.com/Chinmay-KB/translations_cleaner) from 0.0.5 to 0.1.0. - [Changelog](https://github.com/Chinmay-KB/translations_cleaner/blob/main/CHANGELOG.md) - [Commits](https://github.com/Chinmay-KB/translations_cleaner/commits) --- updated-dependencies: - dependency-name: translations_cleaner dependency-version: 0.1.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> * chore(translations): Translated using Weblate (German) Currently translated at 99.2% (821 of 827 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/de/ * chore(translations): Translated using Weblate (Estonian) Currently translated at 100.0% (827 of 827 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/et/ * build: Bump version to 2.4.0 * build: (deps): bump sqflite_common_ffi from 2.3.6 to 2.3.7+1 Bumps [sqflite_common_ffi](https://github.com/tekartik/sqflite) from 2.3.6 to 2.3.7+1. - [Commits](https://github.com/tekartik/sqflite/compare/sqflite_common_ffi_v2.3.6...sqflite_common_ffi/v2.3.7) --- updated-dependencies: - dependency-name: sqflite_common_ffi dependency-version: 2.3.7+1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> * chore(translations): Translated using Weblate (Czech) Currently translated at 66.1% (547 of 827 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/cs/ * chore(translations): Translated using Weblate (Czech) Currently translated at 72.7% (602 of 827 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/cs/ * chore(translations): Translated using Weblate (German) Currently translated at 99.8% (826 of 827 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/de/ * chore: Add security.md file * fix: Locale unlocalized strings * build: (deps): bump matrix from 4.1.0 to 5.0.0 Bumps [matrix](https://github.com/famedly/matrix-dart-sdk) from 4.1.0 to 5.0.0. - [Release notes](https://github.com/famedly/matrix-dart-sdk/releases) - [Changelog](https://github.com/famedly/matrix-dart-sdk/blob/main/CHANGELOG.md) - [Commits](https://github.com/famedly/matrix-dart-sdk/compare/v4.1.0...v5.0.0) --- updated-dependencies: - dependency-name: matrix dependency-version: 5.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> * fix: Notifications on web correctly managed when tab not focused * chore: Add changelog for android * chore: Remove duplicated localization * fix: Sign in label * chore: Versionize fcm shared isolate * build: Remove unused packag * build: (deps): bump package_info_plus from 8.3.1 to 9.0.0 Bumps [package_info_plus](https://github.com/fluttercommunity/plus_plugins/tree/main/packages/package_info_plus) from 8.3.1 to 9.0.0. - [Release notes](https://github.com/fluttercommunity/plus_plugins/releases) - [Commits](https://github.com/fluttercommunity/plus_plugins/commits/package_info_plus-v9.0.0/packages/package_info_plus) --- updated-dependencies: - dependency-name: package_info_plus dependency-version: 9.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> * feat: Display particle animation on login page * chore: Use fixed version of fcm shared isolate * fix: apk crash on some platforms due new flutter version * chore: Correct kotlin format * fix iOS notifications * fluffychat merge * fluffychat merge * fluffychat merge * fluffychat merge * fluffychat merge * fluffychat merge * add missing type annotations * update matrix version * fluffychat merge * fluffychat merge * fix notification on click actions --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Christian Kußowski <c.kussowski@famedly.com> Co-authored-by: Krille-chan <christian-kussowski@posteo.de> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: BeMeritus <bemerituss@gmail.com> Co-authored-by: Frank Paul Silye <frankps@gmail.com> Co-authored-by: josé m. <correoxm@disroot.org> Co-authored-by: xabirequejo <xabi.rn@gmail.com> Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com> Co-authored-by: Priit Jõerüüt <jrthwlate@users.noreply.hosted.weblate.org> Co-authored-by: Jelv <post@jelv.nl> Co-authored-by: Дмитрий Михирев <bizdelnick@gmail.com> Co-authored-by: Kimby <kimbyqs@gmail.com> Co-authored-by: Christian <christian-pauly@posteo.de> Co-authored-by: Kom nake <kominak310@svcache.com> Co-authored-by: hugues de keyzer <komputilisto@hugues.info> Co-authored-by: nautilusx <translate@disroot.org> Co-authored-by: Šebestová <ka.sebestova.cz@gmail.com>
613 lines
17 KiB
Dart
613 lines
17 KiB
Dart
/*
|
|
* Famedly
|
|
* Copyright (C) 2019, 2020, 2021 Famedly GmbH
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as
|
|
* published by the Free Software Foundation, either version 3 of the
|
|
* License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
import 'dart:async';
|
|
import 'dart:math';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
|
|
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
|
import 'package:flutter_webrtc/flutter_webrtc.dart' hide VideoRenderer;
|
|
import 'package:just_audio/just_audio.dart';
|
|
import 'package:matrix/matrix.dart';
|
|
import 'package:wakelock_plus/wakelock_plus.dart';
|
|
|
|
import 'package:fluffychat/l10n/l10n.dart';
|
|
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
|
import 'package:fluffychat/utils/platform_infos.dart';
|
|
import 'package:fluffychat/utils/voip/video_renderer.dart';
|
|
import 'package:fluffychat/widgets/avatar.dart';
|
|
import 'pip/pip_view.dart';
|
|
|
|
class _StreamView extends StatelessWidget {
|
|
const _StreamView(
|
|
this.wrappedStream, {
|
|
this.mainView = false,
|
|
required this.matrixClient,
|
|
});
|
|
|
|
final WrappedMediaStream wrappedStream;
|
|
final Client matrixClient;
|
|
|
|
final bool mainView;
|
|
|
|
Uri? get avatarUrl => wrappedStream.getUser().avatarUrl;
|
|
|
|
String? get displayName => wrappedStream.displayName;
|
|
|
|
String get avatarName => wrappedStream.avatarName;
|
|
|
|
bool get isLocal => wrappedStream.isLocal();
|
|
|
|
bool get mirrored =>
|
|
wrappedStream.isLocal() &&
|
|
wrappedStream.purpose == SDPStreamMetadataPurpose.Usermedia;
|
|
|
|
bool get audioMuted => wrappedStream.audioMuted;
|
|
|
|
bool get videoMuted => wrappedStream.videoMuted;
|
|
|
|
bool get isScreenSharing =>
|
|
wrappedStream.purpose == SDPStreamMetadataPurpose.Screenshare;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
decoration: const BoxDecoration(color: Colors.black54),
|
|
child: Stack(
|
|
alignment: Alignment.center,
|
|
children: <Widget>[
|
|
VideoRenderer(
|
|
wrappedStream,
|
|
mirror: mirrored,
|
|
fit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain,
|
|
),
|
|
if (videoMuted) ...[
|
|
Container(color: Colors.black54),
|
|
Positioned(
|
|
child: Avatar(
|
|
mxContent: avatarUrl,
|
|
name: displayName,
|
|
size: mainView ? 96 : 48,
|
|
client: matrixClient,
|
|
// textSize: mainView ? 36 : 24,
|
|
// matrixClient: matrixClient,
|
|
),
|
|
),
|
|
],
|
|
if (!isScreenSharing)
|
|
Positioned(
|
|
left: 4.0,
|
|
bottom: 4.0,
|
|
child: Icon(
|
|
audioMuted ? Icons.mic_off : Icons.mic,
|
|
color: Colors.white,
|
|
size: 18.0,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class Calling extends StatefulWidget {
|
|
final VoidCallback? onClear;
|
|
final BuildContext context;
|
|
final String callId;
|
|
final CallSession call;
|
|
final Client client;
|
|
|
|
const Calling({
|
|
required this.context,
|
|
required this.call,
|
|
required this.client,
|
|
required this.callId,
|
|
this.onClear,
|
|
super.key,
|
|
});
|
|
|
|
@override
|
|
MyCallingPage createState() => MyCallingPage();
|
|
}
|
|
|
|
class MyCallingPage extends State<Calling> {
|
|
Room? get room => call.room;
|
|
|
|
String get displayName =>
|
|
call.room.getLocalizedDisplayname(MatrixLocals(L10n.of(widget.context)));
|
|
|
|
String get callId => widget.callId;
|
|
|
|
CallSession get call => widget.call;
|
|
|
|
MediaStream? get localStream {
|
|
if (call.localUserMediaStream != null) {
|
|
return call.localUserMediaStream!.stream!;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
MediaStream? get remoteStream {
|
|
if (call.getRemoteStreams.isNotEmpty) {
|
|
return call.getRemoteStreams[0].stream!;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
bool get isMicrophoneMuted => call.isMicrophoneMuted;
|
|
|
|
bool get isLocalVideoMuted => call.isLocalVideoMuted;
|
|
|
|
bool get isScreensharingEnabled => call.screensharingEnabled;
|
|
|
|
bool get isRemoteOnHold => call.remoteOnHold;
|
|
|
|
bool get voiceonly => call.type == CallType.kVoice;
|
|
|
|
bool get connecting => call.state == CallState.kConnecting;
|
|
|
|
bool get connected => call.state == CallState.kConnected;
|
|
|
|
double? _localVideoHeight;
|
|
double? _localVideoWidth;
|
|
EdgeInsetsGeometry? _localVideoMargin;
|
|
CallState? _state;
|
|
|
|
void _playCallSound() async {
|
|
const path = 'assets/sounds/call.ogg';
|
|
if (kIsWeb || PlatformInfos.isMobile || PlatformInfos.isMacOS) {
|
|
final player = AudioPlayer();
|
|
await player.setAsset(path);
|
|
player.play();
|
|
} else {
|
|
Logs().w('Playing sound not implemented for this platform!');
|
|
}
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
initialize();
|
|
_playCallSound();
|
|
}
|
|
|
|
void initialize() async {
|
|
final call = this.call;
|
|
call.onCallStateChanged.stream.listen(_handleCallState);
|
|
call.onCallEventChanged.stream.listen((event) {
|
|
if (event == CallStateChange.kFeedsChanged) {
|
|
setState(() {
|
|
call.tryRemoveStopedStreams();
|
|
});
|
|
} else if (event == CallStateChange.kLocalHoldUnhold ||
|
|
event == CallStateChange.kRemoteHoldUnhold) {
|
|
setState(() {});
|
|
Logs().i(
|
|
'Call hold event: local ${call.localHold}, remote ${call.remoteOnHold}',
|
|
);
|
|
}
|
|
});
|
|
_state = call.state;
|
|
|
|
if (call.type == CallType.kVideo) {
|
|
try {
|
|
// Enable wakelock (keep screen on)
|
|
unawaited(WakelockPlus.enable());
|
|
} catch (_) {}
|
|
}
|
|
}
|
|
|
|
void cleanUp() {
|
|
Timer(const Duration(seconds: 2), () => widget.onClear?.call());
|
|
if (call.type == CallType.kVideo) {
|
|
try {
|
|
unawaited(WakelockPlus.disable());
|
|
} catch (_) {}
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
super.dispose();
|
|
call.cleanUp.call();
|
|
}
|
|
|
|
void _resizeLocalVideo(Orientation orientation) {
|
|
final shortSide = min(
|
|
MediaQuery.sizeOf(widget.context).width,
|
|
MediaQuery.sizeOf(widget.context).height,
|
|
);
|
|
_localVideoMargin = remoteStream != null
|
|
? const EdgeInsets.only(top: 20.0, right: 20.0)
|
|
: EdgeInsets.zero;
|
|
_localVideoWidth = remoteStream != null
|
|
? shortSide / 3
|
|
: MediaQuery.sizeOf(widget.context).width;
|
|
_localVideoHeight = remoteStream != null
|
|
? shortSide / 4
|
|
: MediaQuery.sizeOf(widget.context).height;
|
|
}
|
|
|
|
void _handleCallState(CallState state) {
|
|
Logs().v('CallingPage::handleCallState: ${state.toString()}');
|
|
if ({CallState.kConnected, CallState.kEnded}.contains(state)) {
|
|
HapticFeedback.heavyImpact();
|
|
}
|
|
|
|
if (mounted) {
|
|
setState(() {
|
|
_state = state;
|
|
if (_state == CallState.kEnded) cleanUp();
|
|
});
|
|
}
|
|
}
|
|
|
|
void _answerCall() {
|
|
setState(() {
|
|
call.answer();
|
|
});
|
|
}
|
|
|
|
void _hangUp() {
|
|
setState(() {
|
|
if (call.isRinging) {
|
|
call.reject();
|
|
} else {
|
|
call.hangup(reason: CallErrorCode.userHangup);
|
|
}
|
|
});
|
|
}
|
|
|
|
void _muteMic() {
|
|
setState(() {
|
|
call.setMicrophoneMuted(!call.isMicrophoneMuted);
|
|
});
|
|
}
|
|
|
|
void _screenSharing() async {
|
|
if (PlatformInfos.isAndroid) {
|
|
if (!call.screensharingEnabled) {
|
|
FlutterForegroundTask.init(
|
|
androidNotificationOptions: AndroidNotificationOptions(
|
|
channelId: 'notification_channel_id',
|
|
channelName: 'Foreground Notification',
|
|
channelDescription: L10n.of(
|
|
widget.context,
|
|
).foregroundServiceRunning,
|
|
),
|
|
iosNotificationOptions: const IOSNotificationOptions(),
|
|
foregroundTaskOptions: ForegroundTaskOptions(
|
|
eventAction: ForegroundTaskEventAction.nothing(),
|
|
),
|
|
);
|
|
FlutterForegroundTask.startService(
|
|
notificationTitle: L10n.of(widget.context).screenSharingTitle,
|
|
notificationText: L10n.of(widget.context).screenSharingDetail,
|
|
);
|
|
} else {
|
|
FlutterForegroundTask.stopService();
|
|
}
|
|
}
|
|
|
|
setState(() {
|
|
call.setScreensharingEnabled(!call.screensharingEnabled);
|
|
});
|
|
}
|
|
|
|
void _remoteOnHold() {
|
|
setState(() {
|
|
call.setRemoteOnHold(!call.remoteOnHold);
|
|
});
|
|
}
|
|
|
|
void _muteCamera() {
|
|
setState(() {
|
|
call.setLocalVideoMuted(!call.isLocalVideoMuted);
|
|
});
|
|
}
|
|
|
|
void _switchCamera() async {
|
|
if (call.localUserMediaStream != null) {
|
|
await Helper.switchCamera(
|
|
call.localUserMediaStream!.stream!.getVideoTracks()[0],
|
|
);
|
|
}
|
|
setState(() {});
|
|
}
|
|
|
|
/*
|
|
void _switchSpeaker() {
|
|
setState(() {
|
|
session.setSpeakerOn();
|
|
});
|
|
}
|
|
*/
|
|
|
|
List<Widget> _buildActionButtons(bool isFloating) {
|
|
if (isFloating) {
|
|
return [];
|
|
}
|
|
|
|
final switchCameraButton = FloatingActionButton(
|
|
heroTag: 'switchCamera',
|
|
onPressed: _switchCamera,
|
|
backgroundColor: Colors.black45,
|
|
child: const Icon(Icons.switch_camera),
|
|
);
|
|
/*
|
|
var switchSpeakerButton = FloatingActionButton(
|
|
heroTag: 'switchSpeaker',
|
|
child: Icon(_speakerOn ? Icons.volume_up : Icons.volume_off),
|
|
onPressed: _switchSpeaker,
|
|
foregroundColor: Colors.black54,
|
|
backgroundColor: Theme.of(widget.context).backgroundColor,
|
|
);
|
|
*/
|
|
final hangupButton = FloatingActionButton(
|
|
heroTag: 'hangup',
|
|
onPressed: _hangUp,
|
|
tooltip: 'Hangup',
|
|
backgroundColor: _state == CallState.kEnded ? Colors.black45 : Colors.red,
|
|
child: const Icon(Icons.call_end),
|
|
);
|
|
|
|
final answerButton = FloatingActionButton(
|
|
heroTag: 'answer',
|
|
onPressed: _answerCall,
|
|
tooltip: 'Answer',
|
|
backgroundColor: Colors.green,
|
|
child: const Icon(Icons.phone),
|
|
);
|
|
|
|
final muteMicButton = FloatingActionButton(
|
|
heroTag: 'muteMic',
|
|
onPressed: _muteMic,
|
|
foregroundColor: isMicrophoneMuted ? Colors.black26 : Colors.white,
|
|
backgroundColor: isMicrophoneMuted ? Colors.white : Colors.black45,
|
|
child: Icon(isMicrophoneMuted ? Icons.mic_off : Icons.mic),
|
|
);
|
|
|
|
final screenSharingButton = FloatingActionButton(
|
|
heroTag: 'screenSharing',
|
|
onPressed: _screenSharing,
|
|
foregroundColor: isScreensharingEnabled ? Colors.black26 : Colors.white,
|
|
backgroundColor: isScreensharingEnabled ? Colors.white : Colors.black45,
|
|
child: const Icon(Icons.desktop_mac),
|
|
);
|
|
|
|
final holdButton = FloatingActionButton(
|
|
heroTag: 'hold',
|
|
onPressed: _remoteOnHold,
|
|
foregroundColor: isRemoteOnHold ? Colors.black26 : Colors.white,
|
|
backgroundColor: isRemoteOnHold ? Colors.white : Colors.black45,
|
|
child: const Icon(Icons.pause),
|
|
);
|
|
|
|
final muteCameraButton = FloatingActionButton(
|
|
heroTag: 'muteCam',
|
|
onPressed: _muteCamera,
|
|
foregroundColor: isLocalVideoMuted ? Colors.black26 : Colors.white,
|
|
backgroundColor: isLocalVideoMuted ? Colors.white : Colors.black45,
|
|
child: Icon(isLocalVideoMuted ? Icons.videocam_off : Icons.videocam),
|
|
);
|
|
|
|
switch (_state) {
|
|
case CallState.kRinging:
|
|
case CallState.kInviteSent:
|
|
case CallState.kCreateAnswer:
|
|
case CallState.kConnecting:
|
|
return call.isOutgoing
|
|
? <Widget>[hangupButton]
|
|
: <Widget>[answerButton, hangupButton];
|
|
case CallState.kConnected:
|
|
return <Widget>[
|
|
muteMicButton,
|
|
//switchSpeakerButton,
|
|
if (!voiceonly && !kIsWeb) switchCameraButton,
|
|
if (!voiceonly) muteCameraButton,
|
|
if (PlatformInfos.isMobile || PlatformInfos.isWeb)
|
|
screenSharingButton,
|
|
holdButton,
|
|
hangupButton,
|
|
];
|
|
case CallState.kEnded:
|
|
return <Widget>[hangupButton];
|
|
case CallState.kFledgling:
|
|
case CallState.kWaitLocalMedia:
|
|
case CallState.kCreateOffer:
|
|
case CallState.kEnding:
|
|
case null:
|
|
break;
|
|
}
|
|
return <Widget>[];
|
|
}
|
|
|
|
List<Widget> _buildContent(Orientation orientation, bool isFloating) {
|
|
final stackWidgets = <Widget>[];
|
|
|
|
final call = this.call;
|
|
if (call.callHasEnded) {
|
|
return stackWidgets;
|
|
}
|
|
|
|
if (call.localHold || call.remoteOnHold) {
|
|
var title = '';
|
|
if (call.localHold) {
|
|
title =
|
|
'${call.room.getLocalizedDisplayname(MatrixLocals(L10n.of(widget.context)))} held the call.';
|
|
} else if (call.remoteOnHold) {
|
|
title = 'You held the call.';
|
|
}
|
|
stackWidgets.add(
|
|
Center(
|
|
child: Column(
|
|
mainAxisAlignment: .center,
|
|
children: [
|
|
const Icon(Icons.pause, size: 48.0, color: Colors.white),
|
|
Text(
|
|
title,
|
|
style: const TextStyle(color: Colors.white, fontSize: 24.0),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
return stackWidgets;
|
|
}
|
|
|
|
var primaryStream =
|
|
call.remoteScreenSharingStream ??
|
|
call.localScreenSharingStream ??
|
|
call.remoteUserMediaStream ??
|
|
call.localUserMediaStream;
|
|
|
|
if (!connected) {
|
|
primaryStream = call.localUserMediaStream;
|
|
}
|
|
|
|
if (primaryStream != null) {
|
|
stackWidgets.add(
|
|
Center(
|
|
child: _StreamView(
|
|
primaryStream,
|
|
mainView: true,
|
|
matrixClient: widget.client,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
if (isFloating || !connected) {
|
|
return stackWidgets;
|
|
}
|
|
|
|
_resizeLocalVideo(orientation);
|
|
|
|
if (call.getRemoteStreams.isEmpty) {
|
|
return stackWidgets;
|
|
}
|
|
|
|
final secondaryStreamViews = <Widget>[];
|
|
|
|
if (call.remoteScreenSharingStream != null) {
|
|
final remoteUserMediaStream = call.remoteUserMediaStream;
|
|
secondaryStreamViews.add(
|
|
SizedBox(
|
|
width: _localVideoWidth,
|
|
height: _localVideoHeight,
|
|
child: _StreamView(
|
|
remoteUserMediaStream!,
|
|
matrixClient: widget.client,
|
|
),
|
|
),
|
|
);
|
|
secondaryStreamViews.add(const SizedBox(height: 10));
|
|
}
|
|
|
|
final localStream =
|
|
call.localUserMediaStream ?? call.localScreenSharingStream;
|
|
if (localStream != null && !isFloating) {
|
|
secondaryStreamViews.add(
|
|
SizedBox(
|
|
width: _localVideoWidth,
|
|
height: _localVideoHeight,
|
|
child: _StreamView(localStream, matrixClient: widget.client),
|
|
),
|
|
);
|
|
secondaryStreamViews.add(const SizedBox(height: 10));
|
|
}
|
|
|
|
if (call.localScreenSharingStream != null && !isFloating) {
|
|
secondaryStreamViews.add(
|
|
SizedBox(
|
|
width: _localVideoWidth,
|
|
height: _localVideoHeight,
|
|
child: _StreamView(
|
|
call.remoteUserMediaStream!,
|
|
matrixClient: widget.client,
|
|
),
|
|
),
|
|
);
|
|
secondaryStreamViews.add(const SizedBox(height: 10));
|
|
}
|
|
|
|
if (secondaryStreamViews.isNotEmpty) {
|
|
stackWidgets.add(
|
|
Container(
|
|
padding: const EdgeInsets.fromLTRB(0, 20, 0, 120),
|
|
alignment: Alignment.bottomRight,
|
|
child: Container(
|
|
width: _localVideoWidth,
|
|
margin: _localVideoMargin,
|
|
child: Column(children: secondaryStreamViews),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
return stackWidgets;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return PIPView(
|
|
builder: (context, isFloating) {
|
|
return Scaffold(
|
|
resizeToAvoidBottomInset: !isFloating,
|
|
floatingActionButtonLocation:
|
|
FloatingActionButtonLocation.centerFloat,
|
|
floatingActionButton: SizedBox(
|
|
width: 320.0,
|
|
height: 150.0,
|
|
child: Row(
|
|
mainAxisAlignment: .spaceAround,
|
|
children: _buildActionButtons(isFloating),
|
|
),
|
|
),
|
|
body: OrientationBuilder(
|
|
builder: (BuildContext context, Orientation orientation) {
|
|
return Container(
|
|
decoration: const BoxDecoration(color: Colors.black87),
|
|
child: Stack(
|
|
children: [
|
|
..._buildContent(orientation, isFloating),
|
|
if (!isFloating)
|
|
Positioned(
|
|
top: 24.0,
|
|
left: 24.0,
|
|
child: IconButton(
|
|
color: Colors.black45,
|
|
icon: const Icon(Icons.arrow_back),
|
|
onPressed: () {
|
|
PIPView.of(context)?.setFloating(true);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|