no message
parent
a37cd47edb
commit
b21e71ee16
@ -0,0 +1,37 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ClippedVideo extends StatefulWidget {
|
||||
final double width;
|
||||
final double height;
|
||||
final Widget child;
|
||||
|
||||
const ClippedVideo({
|
||||
Key? key,
|
||||
required this.width,
|
||||
required this.height,
|
||||
required this.child,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_ClippedVideoState createState() => _ClippedVideoState();
|
||||
}
|
||||
|
||||
class _ClippedVideoState extends State<ClippedVideo> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
||||
border: Border.all(
|
||||
color: Colors.white24,
|
||||
),
|
||||
),
|
||||
child: ClipRRect(
|
||||
child: widget.child,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,237 @@
|
||||
import 'dart:async';
|
||||
import 'package:after_layout/after_layout.dart';
|
||||
import 'package:doctor_app_flutter/screens/live_care/web-rtc/widgets/circle_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ConferenceButtonBar extends StatefulWidget {
|
||||
final VoidCallback onVideoEnabled;
|
||||
final VoidCallback onAudioEnabled;
|
||||
final VoidCallback onHangup;
|
||||
final VoidCallback onSwitchCamera;
|
||||
final VoidCallback onPersonAdd;
|
||||
final VoidCallback onPersonRemove;
|
||||
final void Function(double) onHeight;
|
||||
final VoidCallback onHide;
|
||||
final VoidCallback onShow;
|
||||
final Stream<bool> videoEnabled;
|
||||
final Stream<bool> audioEnabled;
|
||||
|
||||
const ConferenceButtonBar({
|
||||
Key? key,
|
||||
required this.onVideoEnabled,
|
||||
required this.onAudioEnabled,
|
||||
required this.onHangup,
|
||||
required this.onSwitchCamera,
|
||||
required this.onPersonAdd,
|
||||
required this.onPersonRemove,
|
||||
required this.videoEnabled,
|
||||
required this.audioEnabled,
|
||||
required this.onHeight,
|
||||
required this.onHide,
|
||||
required this.onShow,
|
||||
}) : assert(videoEnabled != null),
|
||||
assert(audioEnabled != null),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
_ConferenceButtonBarState createState() => _ConferenceButtonBarState();
|
||||
}
|
||||
|
||||
class _ConferenceButtonBarState extends State<ConferenceButtonBar> with AfterLayoutMixin<ConferenceButtonBar> {
|
||||
var _bottom = -100.0;
|
||||
late Timer? _timer;
|
||||
late int _remaining;
|
||||
var _videoEnabled = true;
|
||||
var _audioEnabled = true;
|
||||
late double _hidden;
|
||||
late double _visible;
|
||||
final _keyButtonBarHeight = GlobalKey();
|
||||
|
||||
final Duration timeout = const Duration(seconds: 5);
|
||||
final Duration ms = const Duration(milliseconds: 1);
|
||||
final Duration periodicDuration = const Duration(milliseconds: 100);
|
||||
|
||||
Timer startTimeout([int? milliseconds]) {
|
||||
final duration = milliseconds == null ? timeout : ms * milliseconds;
|
||||
_remaining = duration.inMilliseconds;
|
||||
return Timer.periodic(periodicDuration, (Timer timer) {
|
||||
_remaining -= periodicDuration.inMilliseconds;
|
||||
if (_remaining <= 0) {
|
||||
timer.cancel();
|
||||
_toggleBar();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _pauseTimer() {
|
||||
if (_timer == null) {
|
||||
return;
|
||||
}
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
}
|
||||
|
||||
void _resumeTimer() {
|
||||
// resume the timer only when there is no timer active or when
|
||||
// the bar is not already hidden.
|
||||
if ((_timer != null && _timer!.isActive) || _bottom == _hidden) {
|
||||
return;
|
||||
}
|
||||
_timer = startTimeout(_remaining);
|
||||
}
|
||||
|
||||
void _toggleBar() {
|
||||
setState(() {
|
||||
_bottom = _bottom == _visible ? _hidden : _visible;
|
||||
if (_bottom == _visible && widget.onShow != null) {
|
||||
widget.onShow();
|
||||
}
|
||||
if (_bottom == _hidden && widget.onHide != null) {
|
||||
widget.onHide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _toggleBarOnEnd() {
|
||||
if (_timer != null) {
|
||||
if (_timer!.isActive) {
|
||||
_timer!.cancel();
|
||||
}
|
||||
_timer = null;
|
||||
}
|
||||
if (_bottom == 0) {
|
||||
_timer = startTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_timer = startTimeout();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
_visible = MediaQuery.of(context).viewPadding.bottom;
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
|
||||
@override
|
||||
void afterFirstLayout(BuildContext context) {
|
||||
final RenderBox? renderBoxButtonBar = _keyButtonBarHeight.currentContext?.findRenderObject() as RenderBox?;
|
||||
final heightButtonBar = renderBoxButtonBar?.size.height ?? 0.0;
|
||||
// Because the `didChangeDependencies` fires before the `afterFirstLayout`, we can use the `_visible` property here.
|
||||
_hidden = -(heightButtonBar + _visible);
|
||||
widget.onHeight(heightButtonBar);
|
||||
_toggleBar();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
if (_timer != null && _timer!.isActive) {
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: GestureDetector(
|
||||
key: Key('show-hide-button-bar-gesture'),
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTapDown: (_) => _pauseTimer(),
|
||||
onTapUp: (_) => _toggleBar(),
|
||||
onTapCancel: () => _resumeTimer(),
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
AnimatedPositioned(
|
||||
key: Key('button-bar'),
|
||||
bottom: _bottom,
|
||||
left: 0,
|
||||
right: 0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.linear,
|
||||
child: _buildRow(context),
|
||||
onEnd: _toggleBarOnEnd,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onPressed(VoidCallback callback) {
|
||||
if (callback != null) {
|
||||
callback();
|
||||
}
|
||||
if (_timer != null && _timer!.isActive) {
|
||||
_timer?.cancel();
|
||||
}
|
||||
_timer = startTimeout();
|
||||
}
|
||||
|
||||
Widget _buildRow(BuildContext context) {
|
||||
return Padding(
|
||||
key: _keyButtonBarHeight,
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: <Widget>[
|
||||
CircleButton(
|
||||
child: StreamBuilder<bool>(
|
||||
stream: widget.videoEnabled,
|
||||
initialData: _videoEnabled,
|
||||
builder: (context, snapshot) {
|
||||
_videoEnabled = snapshot.data!;
|
||||
return Icon(
|
||||
_videoEnabled ? Icons.videocam : Icons.videocam_off,
|
||||
color: Colors.white,
|
||||
);
|
||||
}),
|
||||
key: Key('camera-button'),
|
||||
onPressed: () => _onPressed(widget.onVideoEnabled),
|
||||
),
|
||||
CircleButton(
|
||||
child: StreamBuilder<bool>(
|
||||
stream: widget.audioEnabled,
|
||||
initialData: _audioEnabled,
|
||||
builder: (context, snapshot) {
|
||||
_audioEnabled = snapshot.data!;
|
||||
return Icon(
|
||||
_audioEnabled ? Icons.mic : Icons.mic_off,
|
||||
color: Colors.white,
|
||||
);
|
||||
}),
|
||||
key: Key('microphone-button'),
|
||||
onPressed: () => _onPressed(widget.onAudioEnabled),
|
||||
),
|
||||
CircleButton(
|
||||
child: const Icon(Icons.switch_camera, color: Colors.white),
|
||||
key: Key('switch-camera-button'),
|
||||
onPressed: () => _onPressed(widget.onSwitchCamera),
|
||||
),
|
||||
CircleButton(
|
||||
radius: 35,
|
||||
child: const RotationTransition(
|
||||
turns: AlwaysStoppedAnimation<double>(135 / 360),
|
||||
child: Icon(
|
||||
Icons.phone,
|
||||
color: Colors.white,
|
||||
size: 40,
|
||||
),
|
||||
),
|
||||
color: Colors.red.withAlpha(200),
|
||||
key: Key('hangup-button'),
|
||||
onPressed: () => _onPressed(widget.onHangup),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,390 @@
|
||||
// import 'dart:async';
|
||||
//
|
||||
// import 'package:doctor_app_flutter/models/LiveCare/room_model.dart';
|
||||
// import 'package:doctor_app_flutter/pages/conference/conference_button_bar.dart';
|
||||
// import 'package:doctor_app_flutter/pages/conference/conference_room.dart';
|
||||
// import 'package:doctor_app_flutter/pages/landing/landing_page.dart';
|
||||
// import 'package:doctor_app_flutter/pages/conference/draggable_publisher.dart';
|
||||
// import 'package:doctor_app_flutter/pages/conference/participant_widget.dart';
|
||||
// import 'package:doctor_app_flutter/pages/conference/widgets/noise_box.dart';
|
||||
// import 'package:doctor_app_flutter/pages/conference/widgets/platform_alert_dialog.dart';
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:flutter/services.dart';
|
||||
// import 'package:wakelock/wakelock.dart';
|
||||
//
|
||||
// class ConferencePage extends StatefulWidget {
|
||||
// final RoomModel roomModel;
|
||||
//
|
||||
// const ConferencePage({Key key, this.roomModel}) : super(key: key);
|
||||
//
|
||||
// @override
|
||||
// _ConferencePageState createState() => _ConferencePageState();
|
||||
// }
|
||||
//
|
||||
// class _ConferencePageState extends State<ConferencePage> {
|
||||
// final StreamController<bool> _onButtonBarVisibleStreamController = StreamController<bool>.broadcast();
|
||||
// final StreamController<double> _onButtonBarHeightStreamController = StreamController<double>.broadcast();
|
||||
// // ConferenceRoom _conferenceRoom;
|
||||
// StreamSubscription _onConferenceRoomException;
|
||||
//
|
||||
// @override
|
||||
// void initState() {
|
||||
// super.initState();
|
||||
// _lockInPortrait();
|
||||
// _connectToRoom();
|
||||
// _wakeLock(true);
|
||||
// }
|
||||
//
|
||||
// void _connectToRoom() async {
|
||||
// try {
|
||||
// final conferenceRoom = ConferenceRoom(
|
||||
// name: widget.roomModel.name,
|
||||
// token: widget.roomModel.token,
|
||||
// identity: widget.roomModel.identity,
|
||||
// );
|
||||
// await conferenceRoom.connect();
|
||||
// setState(() {
|
||||
// _conferenceRoom = conferenceRoom;
|
||||
// _onConferenceRoomException = _conferenceRoom.onException.listen((err) async {
|
||||
// await PlatformAlertDialog(
|
||||
// title: err is PlatformException ? err.message : 'An error occured',
|
||||
// content: err is PlatformException ? err.details : err.toString(),
|
||||
// defaultActionText: 'OK',
|
||||
// ).show(context);
|
||||
// });
|
||||
// _conferenceRoom.addListener(_conferenceRoomUpdated);
|
||||
// });
|
||||
// } catch (err) {
|
||||
// print(err);
|
||||
// await PlatformAlertDialog(
|
||||
// title: err is PlatformException ? err.message : 'An error occured',
|
||||
// content: err is PlatformException ? err.details : err.toString(),
|
||||
// defaultActionText: 'OK',
|
||||
// ).show(context);
|
||||
//
|
||||
// Navigator.of(context).pop();
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Future<void> _lockInPortrait() async {
|
||||
// await SystemChrome.setPreferredOrientations(<DeviceOrientation>[
|
||||
// DeviceOrientation.portraitUp,
|
||||
// DeviceOrientation.portraitDown,
|
||||
// ]);
|
||||
// }
|
||||
//
|
||||
// @override
|
||||
// void dispose() {
|
||||
// _freePortraitLock();
|
||||
// _wakeLock(false);
|
||||
// _disposeStreamsAndSubscriptions();
|
||||
// if (_conferenceRoom != null) _conferenceRoom.removeListener(_conferenceRoomUpdated);
|
||||
// super.dispose();
|
||||
// }
|
||||
//
|
||||
// Future<void> _freePortraitLock() async {
|
||||
// await SystemChrome.setPreferredOrientations(<DeviceOrientation>[
|
||||
// DeviceOrientation.landscapeRight,
|
||||
// DeviceOrientation.landscapeLeft,
|
||||
// DeviceOrientation.portraitUp,
|
||||
// DeviceOrientation.portraitDown,
|
||||
// ]);
|
||||
// }
|
||||
//
|
||||
// Future<void> _disposeStreamsAndSubscriptions() async {
|
||||
// if (_onButtonBarVisibleStreamController != null) await _onButtonBarVisibleStreamController.close();
|
||||
// if (_onButtonBarHeightStreamController != null) await _onButtonBarHeightStreamController.close();
|
||||
// if (_onConferenceRoomException != null) await _onConferenceRoomException.cancel();
|
||||
// }
|
||||
//
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// return WillPopScope(
|
||||
// onWillPop: () async => false,
|
||||
// child: Scaffold(
|
||||
// backgroundColor: Colors.white,
|
||||
// body: _conferenceRoom == null ? showProgress() : buildLayout(),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// LayoutBuilder buildLayout() {
|
||||
// return LayoutBuilder(
|
||||
// builder: (BuildContext context, BoxConstraints constraints) {
|
||||
// return Stack(
|
||||
// children: <Widget>[
|
||||
// _buildParticipants(context, constraints.biggest, _conferenceRoom),
|
||||
// ConferenceButtonBar(
|
||||
// audioEnabled: _conferenceRoom.onAudioEnabled,
|
||||
// videoEnabled: _conferenceRoom.onVideoEnabled,
|
||||
// onAudioEnabled: _conferenceRoom.toggleAudioEnabled,
|
||||
// onVideoEnabled: _conferenceRoom.toggleVideoEnabled,
|
||||
// onHangup: _onHangup,
|
||||
// onSwitchCamera: _conferenceRoom.switchCamera,
|
||||
// onPersonAdd: _onPersonAdd,
|
||||
// onPersonRemove: _onPersonRemove,
|
||||
// onHeight: _onHeightBar,
|
||||
// onShow: _onShowBar,
|
||||
// onHide: _onHideBar,
|
||||
// ),
|
||||
// ],
|
||||
// );
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// Widget showProgress() {
|
||||
// return Column(
|
||||
// mainAxisAlignment: MainAxisAlignment.center,
|
||||
// crossAxisAlignment: CrossAxisAlignment.center,
|
||||
// children: <Widget>[
|
||||
// Center(child: CircularProgressIndicator()),
|
||||
// SizedBox(
|
||||
// height: 10,
|
||||
// ),
|
||||
// Text(
|
||||
// 'Connecting to the call...',
|
||||
// style: TextStyle(color: Colors.white),
|
||||
// ),
|
||||
// ],
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// Future<void> _onHangup() async {
|
||||
// print('onHangup');
|
||||
// await _conferenceRoom.disconnect();
|
||||
// LandingPage.isOpenCallPage = false;
|
||||
// Navigator.of(context).pop();
|
||||
// }
|
||||
//
|
||||
// void _onPersonAdd() {
|
||||
// print('onPersonAdd');
|
||||
// try {
|
||||
// _conferenceRoom.addDummy(
|
||||
// child: Stack(
|
||||
// children: <Widget>[
|
||||
// const Placeholder(),
|
||||
// Center(
|
||||
// child: Text(
|
||||
// (_conferenceRoom.participants.length + 1).toString(),
|
||||
// style: const TextStyle(
|
||||
// shadows: <Shadow>[
|
||||
// Shadow(
|
||||
// blurRadius: 3.0,
|
||||
// color: Color.fromARGB(255, 0, 0, 0),
|
||||
// ),
|
||||
// Shadow(
|
||||
// blurRadius: 8.0,
|
||||
// color: Color.fromARGB(255, 255, 255, 255),
|
||||
// ),
|
||||
// ],
|
||||
// fontSize: 80,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// );
|
||||
// } on PlatformException catch (err) {
|
||||
// PlatformAlertDialog(
|
||||
// title: err.message,
|
||||
// content: err.details,
|
||||
// defaultActionText: 'OK',
|
||||
// ).show(context);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// void _onPersonRemove() {
|
||||
// print('onPersonRemove');
|
||||
// _conferenceRoom.removeDummy();
|
||||
// }
|
||||
//
|
||||
// Widget _buildParticipants(BuildContext context, Size size, ConferenceRoom conferenceRoom) {
|
||||
// final children = <Widget>[];
|
||||
// final length = conferenceRoom.participants.length;
|
||||
//
|
||||
// if (length <= 2) {
|
||||
// _buildOverlayLayout(context, size, children);
|
||||
// return Stack(children: children);
|
||||
// }
|
||||
//
|
||||
// void buildInCols(bool removeLocalBeforeChunking, bool moveLastOfEachRowToNextRow, int columns) {
|
||||
// _buildLayoutInGrid(
|
||||
// context,
|
||||
// size,
|
||||
// children,
|
||||
// removeLocalBeforeChunking: removeLocalBeforeChunking,
|
||||
// moveLastOfEachRowToNextRow: moveLastOfEachRowToNextRow,
|
||||
// columns: columns,
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// // if (length <= 3) {
|
||||
// // buildInCols(true, false, 1);
|
||||
// // } else if (length == 5) {
|
||||
// // buildInCols(false, true, 2);
|
||||
// // } else if (length <= 6 || length == 8) {
|
||||
// // buildInCols(false, false, 2);
|
||||
// // } else if (length == 7 || length == 9) {
|
||||
// // buildInCols(true, false, 2);
|
||||
// // } else if (length == 10) {
|
||||
// // buildInCols(false, true, 3);
|
||||
// // } else if (length == 13 || length == 16) {
|
||||
// // buildInCols(true, false, 3);
|
||||
// // } else if (length <= 18) {
|
||||
// // buildInCols(false, false, 3);
|
||||
// // }
|
||||
//
|
||||
// return Column(
|
||||
// children: children,
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// void _buildOverlayLayout(BuildContext context, Size size, List<Widget> children) {
|
||||
// final participants = _conferenceRoom.participants;
|
||||
// if (participants.length == 1) {
|
||||
// children.add(_buildNoiseBox());
|
||||
// } else {
|
||||
// final remoteParticipant = participants.firstWhere((ParticipantWidget participant) => participant.isRemote, orElse: () => null);
|
||||
// if (remoteParticipant != null) {
|
||||
// children.add(remoteParticipant);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// final localParticipant = participants.firstWhere((ParticipantWidget participant) => !participant.isRemote, orElse: () => null);
|
||||
// if (localParticipant != null) {
|
||||
// children.add(DraggablePublisher(
|
||||
// key: Key('publisher'),
|
||||
// child: localParticipant,
|
||||
// availableScreenSize: size,
|
||||
// onButtonBarVisible: _onButtonBarVisibleStreamController.stream,
|
||||
// onButtonBarHeight: _onButtonBarHeightStreamController.stream,
|
||||
// ));
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// void _buildLayoutInGrid(
|
||||
// BuildContext context,
|
||||
// Size size,
|
||||
// List<Widget> children, {
|
||||
// bool removeLocalBeforeChunking = false,
|
||||
// bool moveLastOfEachRowToNextRow = false,
|
||||
// int columns = 2,
|
||||
// }) {
|
||||
// final participants = _conferenceRoom.participants;
|
||||
// ParticipantWidget localParticipant;
|
||||
// if (removeLocalBeforeChunking) {
|
||||
// localParticipant = participants.firstWhere((ParticipantWidget participant) => !participant.isRemote, orElse: () => null);
|
||||
// if (localParticipant != null) {
|
||||
// participants.remove(localParticipant);
|
||||
// }
|
||||
// }
|
||||
// final chunkedParticipants = chunk(array: participants, size: columns);
|
||||
// if (localParticipant != null) {
|
||||
// chunkedParticipants.last.add(localParticipant);
|
||||
// participants.add(localParticipant);
|
||||
// }
|
||||
//
|
||||
// if (moveLastOfEachRowToNextRow) {
|
||||
// for (var i = 0; i < chunkedParticipants.length - 1; i++) {
|
||||
// var participant = chunkedParticipants[i].removeLast();
|
||||
// chunkedParticipants[i + 1].insert(0, participant);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// for (final participantChunk in chunkedParticipants) {
|
||||
// final rowChildren = <Widget>[];
|
||||
// for (final participant in participantChunk) {
|
||||
// rowChildren.add(
|
||||
// Container(
|
||||
// width: size.width / participantChunk.length,
|
||||
// height: size.height / chunkedParticipants.length,
|
||||
// child: participant,
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// children.add(
|
||||
// Container(
|
||||
// height: size.height / chunkedParticipants.length,
|
||||
// child: Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
// children: rowChildren,
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// NoiseBox _buildNoiseBox() {
|
||||
// return NoiseBox(
|
||||
// density: NoiseBoxDensity.xLow,
|
||||
// backgroundColor: Colors.grey.shade900,
|
||||
// child: Center(
|
||||
// child: Container(
|
||||
// color: Colors.black54,
|
||||
// width: double.infinity,
|
||||
// height: 40,
|
||||
// child: Center(
|
||||
// child: Text(
|
||||
// 'Waiting for another participant to connect to the call...',
|
||||
// key: Key('text-wait'),
|
||||
// textAlign: TextAlign.center,
|
||||
// style: TextStyle(color: Colors.white),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// List<List<T>> chunk<T>({required List<T> array, required int size}) {
|
||||
// final result = <List<T>>[];
|
||||
// if (array.isEmpty || size <= 0) {
|
||||
// return result;
|
||||
// }
|
||||
// var first = 0;
|
||||
// var last = size;
|
||||
// final totalLoop = array.length % size == 0 ? array.length ~/ size : array.length ~/ size + 1;
|
||||
// for (var i = 0; i < totalLoop; i++) {
|
||||
// if (last > array.length) {
|
||||
// result.add(array.sublist(first, array.length));
|
||||
// } else {
|
||||
// result.add(array.sublist(first, last));
|
||||
// }
|
||||
// first = last;
|
||||
// last = last + size;
|
||||
// }
|
||||
// return result;
|
||||
// }
|
||||
//
|
||||
// void _onHeightBar(double height) {
|
||||
// _onButtonBarHeightStreamController.add(height);
|
||||
// }
|
||||
//
|
||||
// void _onShowBar() {
|
||||
// setState(() {
|
||||
// SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.bottom, SystemUiOverlay.top]);
|
||||
// });
|
||||
// _onButtonBarVisibleStreamController.add(true);
|
||||
// }
|
||||
//
|
||||
// void _onHideBar() {
|
||||
// setState(() {
|
||||
// SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.bottom]);
|
||||
// });
|
||||
// _onButtonBarVisibleStreamController.add(false);
|
||||
// }
|
||||
//
|
||||
// Future<void> _wakeLock(bool enable) async {
|
||||
// try {
|
||||
// return await (enable ? Wakelock.enable() : Wakelock.disable());
|
||||
// } catch (err) {
|
||||
// print('Unable to change the Wakelock and set it to $enable');
|
||||
// print(err);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// void _conferenceRoomUpdated() {
|
||||
// setState(() {});
|
||||
// }
|
||||
// }
|
||||
@ -0,0 +1,529 @@
|
||||
// import 'dart:async';
|
||||
// import 'dart:typed_data';
|
||||
//
|
||||
// import 'package:doctor_app_flutter/pages/conference/participant_widget.dart';
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:flutter/services.dart';
|
||||
//
|
||||
// class ConferenceRoom with ChangeNotifier {
|
||||
// final String name;
|
||||
// final String token;
|
||||
// final String identity;
|
||||
//
|
||||
// final StreamController<bool> _onAudioEnabledStreamController = StreamController<bool>.broadcast();
|
||||
// Stream<bool> onAudioEnabled;
|
||||
// final StreamController<bool> _onVideoEnabledStreamController = StreamController<bool>.broadcast();
|
||||
// Stream<bool> onVideoEnabled;
|
||||
// final StreamController<Exception> _onExceptionStreamController = StreamController<Exception>.broadcast();
|
||||
// Stream<Exception> onException;
|
||||
//
|
||||
// final Completer<Room> _completer = Completer<Room>();
|
||||
//
|
||||
// final List<ParticipantWidget> _participants = [];
|
||||
// final List<ParticipantBuffer> _participantBuffer = [];
|
||||
// final List<StreamSubscription> _streamSubscriptions = [];
|
||||
// final List<RemoteDataTrack> _dataTracks = [];
|
||||
// final List<String> _messages = [];
|
||||
//
|
||||
// CameraCapturer _cameraCapturer;
|
||||
// Room _room;
|
||||
// Timer _timer;
|
||||
//
|
||||
// ConferenceRoom({
|
||||
// required this.name,
|
||||
// required this.token,
|
||||
// required this.identity,
|
||||
// }) {
|
||||
// onAudioEnabled = _onAudioEnabledStreamController.stream;
|
||||
// onVideoEnabled = _onVideoEnabledStreamController.stream;
|
||||
// onException = _onExceptionStreamController.stream;
|
||||
// }
|
||||
//
|
||||
// List<ParticipantWidget> get participants {
|
||||
// return [..._participants];
|
||||
// }
|
||||
//
|
||||
// Future<Room> connect() async {
|
||||
// print('ConferenceRoom.connect()');
|
||||
// try {
|
||||
// await TwilioProgrammableVideo.debug(dart: true, native: true);
|
||||
// await TwilioProgrammableVideo.setSpeakerphoneOn(true);
|
||||
//
|
||||
// _cameraCapturer = CameraCapturer(CameraSource.FRONT_CAMERA);
|
||||
// var connectOptions = ConnectOptions(
|
||||
// token,
|
||||
// roomName: name,
|
||||
// preferredAudioCodecs: [OpusCodec()],
|
||||
// audioTracks: [LocalAudioTrack(true)],
|
||||
// dataTracks: [LocalDataTrack()],
|
||||
// videoTracks: [LocalVideoTrack(true, _cameraCapturer)],
|
||||
// enableDominantSpeaker: true,
|
||||
// );
|
||||
//
|
||||
// _room = await TwilioProgrammableVideo.connect(connectOptions);
|
||||
//
|
||||
// _streamSubscriptions.add(_room.onConnected.listen(_onConnected));
|
||||
// _streamSubscriptions.add(_room.onConnectFailure.listen(_onConnectFailure));
|
||||
//
|
||||
// return _completer.future;
|
||||
// } catch (err) {
|
||||
// print(err);
|
||||
// rethrow;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Future<void> disconnect() async {
|
||||
// print('ConferenceRoom.disconnect()');
|
||||
// if (_timer != null) {
|
||||
// _timer.cancel();
|
||||
// }
|
||||
// await _room.disconnect();
|
||||
// }
|
||||
//
|
||||
// @override
|
||||
// void dispose() {
|
||||
// print('ConferenceRoom.dispose()');
|
||||
// _disposeStreamsAndSubscriptions();
|
||||
// super.dispose();
|
||||
// }
|
||||
//
|
||||
// Future<void> _disposeStreamsAndSubscriptions() async {
|
||||
// await _onAudioEnabledStreamController.close();
|
||||
// await _onVideoEnabledStreamController.close();
|
||||
// await _onExceptionStreamController.close();
|
||||
// for (var streamSubscription in _streamSubscriptions) {
|
||||
// await streamSubscription.cancel();
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Future<void> sendMessage(String message) async {
|
||||
// final tracks = _room.localParticipant.localDataTracks;
|
||||
// final localDataTrack = tracks.isEmpty ? null : tracks[0].localDataTrack;
|
||||
// if (localDataTrack == null || _messages.isNotEmpty) {
|
||||
// print('ConferenceRoom.sendMessage => Track is not available yet, buffering message.');
|
||||
// _messages.add(message);
|
||||
// return;
|
||||
// }
|
||||
// await localDataTrack.send(message);
|
||||
// }
|
||||
//
|
||||
// Future<void> sendBufferMessage(ByteBuffer message) async {
|
||||
// final tracks = _room.localParticipant.localDataTracks;
|
||||
// final localDataTrack = tracks.isEmpty ? null : tracks[0].localDataTrack;
|
||||
// if (localDataTrack == null) {
|
||||
// return;
|
||||
// }
|
||||
// await localDataTrack.sendBuffer(message);
|
||||
// }
|
||||
//
|
||||
// Future<void> toggleVideoEnabled() async {
|
||||
// final tracks = _room.localParticipant.localVideoTracks;
|
||||
// final localVideoTrack = tracks.isEmpty ? null : tracks[0].localVideoTrack;
|
||||
// if (localVideoTrack == null) {
|
||||
// print('ConferenceRoom.toggleVideoEnabled() => Track is not available yet!');
|
||||
// return;
|
||||
// }
|
||||
// await localVideoTrack.enable(!localVideoTrack.isEnabled);
|
||||
//
|
||||
// var index = _participants.indexWhere((ParticipantWidget participant) => !participant.isRemote);
|
||||
// if (index < 0) {
|
||||
// return;
|
||||
// }
|
||||
// var participant = _participants[index];
|
||||
// _participants.replaceRange(
|
||||
// index,
|
||||
// index + 1,
|
||||
// [
|
||||
// participant.copyWith(videoEnabled: localVideoTrack.isEnabled),
|
||||
// ],
|
||||
// );
|
||||
// print('ConferenceRoom.toggleVideoEnabled() => ${localVideoTrack.isEnabled}');
|
||||
// _onVideoEnabledStreamController.add(localVideoTrack.isEnabled);
|
||||
// notifyListeners();
|
||||
// }
|
||||
//
|
||||
// Future<void> toggleAudioEnabled() async {
|
||||
// final tracks = _room.localParticipant.localAudioTracks;
|
||||
// final localAudioTrack = tracks.isEmpty ? null : tracks[0].localAudioTrack;
|
||||
// if (localAudioTrack == null) {
|
||||
// print('ConferenceRoom.toggleAudioEnabled() => Track is not available yet!');
|
||||
// return;
|
||||
// }
|
||||
// await localAudioTrack.enable(!localAudioTrack.isEnabled);
|
||||
//
|
||||
// var index = _participants.indexWhere((ParticipantWidget participant) => !participant.isRemote);
|
||||
// if (index < 0) {
|
||||
// return;
|
||||
// }
|
||||
// var participant = _participants[index];
|
||||
// _participants.replaceRange(
|
||||
// index,
|
||||
// index + 1,
|
||||
// [
|
||||
// participant.copyWith(audioEnabled: localAudioTrack.isEnabled),
|
||||
// ],
|
||||
// );
|
||||
// print('ConferenceRoom.toggleAudioEnabled() => ${localAudioTrack.isEnabled}');
|
||||
// _onAudioEnabledStreamController.add(localAudioTrack.isEnabled);
|
||||
// notifyListeners();
|
||||
// }
|
||||
//
|
||||
// Future<void> switchCamera() async {
|
||||
// print('ConferenceRoom.switchCamera()');
|
||||
// try {
|
||||
// await _cameraCapturer.switchCamera();
|
||||
// } on FormatException catch (e) {
|
||||
// print(
|
||||
// 'ConferenceRoom.switchCamera() failed because of FormatException with message: ${e.message}',
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// void addDummy({Widget child}) {
|
||||
// print('ConferenceRoom.addDummy()');
|
||||
// if (_participants.length >= 18) {
|
||||
// throw PlatformException(
|
||||
// code: 'ConferenceRoom.maximumReached',
|
||||
// message: 'Maximum reached',
|
||||
// details: 'Currently the lay-out can only render a maximum of 18 participants',
|
||||
// );
|
||||
// }
|
||||
// _participants.insert(
|
||||
// 0,
|
||||
// ParticipantWidget(
|
||||
// id: (_participants.length + 1).toString(),
|
||||
// child: child,
|
||||
// isRemote: true,
|
||||
// audioEnabled: true,
|
||||
// videoEnabled: true,
|
||||
// isDummy: true,
|
||||
// ),
|
||||
// );
|
||||
// notifyListeners();
|
||||
// }
|
||||
//
|
||||
// void removeDummy() {
|
||||
// print('ConferenceRoom.removeDummy()');
|
||||
// var dummy = _participants.firstWhere((participant) => participant.isDummy, orElse: () => null);
|
||||
// if (dummy != null) {
|
||||
// _participants.remove(dummy);
|
||||
// notifyListeners();
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// void _onConnected(Room room) {
|
||||
// print('ConferenceRoom._onConnected => state: ${room.state}');
|
||||
//
|
||||
// // When connected for the first time, add remote participant listeners
|
||||
// _streamSubscriptions.add(_room.onParticipantConnected.listen(_onParticipantConnected));
|
||||
// _streamSubscriptions.add(_room.onParticipantDisconnected.listen(_onParticipantDisconnected));
|
||||
// _streamSubscriptions.add(_room.onDominantSpeakerChange.listen(_onDominantSpeakerChanged));
|
||||
// // Only add ourselves when connected for the first time too.
|
||||
// _participants.add(
|
||||
// _buildParticipant(
|
||||
// child: room.localParticipant.localVideoTracks[0].localVideoTrack.widget(),
|
||||
// id: identity,
|
||||
// audioEnabled: true,
|
||||
// videoEnabled: true,
|
||||
// ),
|
||||
// );
|
||||
// for (final remoteParticipant in room.remoteParticipants) {
|
||||
// var participant = _participants.firstWhere((participant) => participant.id == remoteParticipant.sid, orElse: () => null);
|
||||
// if (participant == null) {
|
||||
// print('Adding participant that was already present in the room ${remoteParticipant.sid}, before I connected');
|
||||
// _addRemoteParticipantListeners(remoteParticipant);
|
||||
// }
|
||||
// }
|
||||
// // We have to listen for the [onDataTrackPublished] event on the [LocalParticipant] in
|
||||
// // order to be able to use the [send] method.
|
||||
// _streamSubscriptions.add(room.localParticipant.onDataTrackPublished.listen(_onLocalDataTrackPublished));
|
||||
// notifyListeners();
|
||||
// _completer.complete(room);
|
||||
//
|
||||
// _timer = Timer.periodic(const Duration(minutes: 1), (_) {
|
||||
// // Let's see if we can send some data over the DataTrack API
|
||||
// sendMessage('And another minute has passed since I connected...');
|
||||
// // Also try the ByteBuffer way of sending data
|
||||
// final list = 'This data has been sent over the ByteBuffer channel of the DataTrack API'.codeUnits;
|
||||
// var bytes = Uint8List.fromList(list);
|
||||
// sendBufferMessage(bytes.buffer);
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// void _onLocalDataTrackPublished(LocalDataTrackPublishedEvent event) {
|
||||
// // Send buffered messages, if any...
|
||||
// while (_messages.isNotEmpty) {
|
||||
// var message = _messages.removeAt(0);
|
||||
// print('Sending buffered message: $message');
|
||||
// event.localDataTrackPublication.localDataTrack.send(message);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// void _onConnectFailure(RoomConnectFailureEvent event) {
|
||||
// print('ConferenceRoom._onConnectFailure: ${event.exception}');
|
||||
// _completer.completeError(event.exception);
|
||||
// }
|
||||
//
|
||||
// void _onDominantSpeakerChanged(DominantSpeakerChangedEvent event) {
|
||||
// print('ConferenceRoom._onDominantSpeakerChanged: ${event.remoteParticipant.identity}');
|
||||
// var oldDominantParticipant = _participants.firstWhere((p) => p.isDominant, orElse: () => null);
|
||||
// if (oldDominantParticipant != null) {
|
||||
// var oldDominantParticipantIndex = _participants.indexOf(oldDominantParticipant);
|
||||
// _participants.replaceRange(oldDominantParticipantIndex, oldDominantParticipantIndex + 1, [oldDominantParticipant.copyWith(isDominant: false)]);
|
||||
// }
|
||||
//
|
||||
// var newDominantParticipant = _participants.firstWhere((p) => p.id == event.remoteParticipant.sid);
|
||||
// var newDominantParticipantIndex = _participants.indexOf(newDominantParticipant);
|
||||
// _participants.replaceRange(newDominantParticipantIndex, newDominantParticipantIndex + 1, [newDominantParticipant.copyWith(isDominant: true)]);
|
||||
// notifyListeners();
|
||||
// }
|
||||
//
|
||||
// void _onParticipantConnected(RoomParticipantConnectedEvent event) {
|
||||
// print('ConferenceRoom._onParticipantConnected, ${event.remoteParticipant.sid}');
|
||||
// _addRemoteParticipantListeners(event.remoteParticipant);
|
||||
// }
|
||||
//
|
||||
// void _onParticipantDisconnected(RoomParticipantDisconnectedEvent event) {
|
||||
// print('ConferenceRoom._onParticipantDisconnected: ${event.remoteParticipant.sid}');
|
||||
// _participants.removeWhere((ParticipantWidget p) => p.id == event.remoteParticipant.sid);
|
||||
// notifyListeners();
|
||||
// }
|
||||
//
|
||||
// ParticipantWidget _buildParticipant({
|
||||
// required Widget child,
|
||||
// required String id,
|
||||
// required bool audioEnabled,
|
||||
// required bool videoEnabled,
|
||||
// RemoteParticipant remoteParticipant,
|
||||
// }) {
|
||||
// return ParticipantWidget(
|
||||
// id: remoteParticipant?.sid,
|
||||
// isRemote: remoteParticipant != null,
|
||||
// child: child,
|
||||
// audioEnabled: audioEnabled,
|
||||
// videoEnabled: videoEnabled,
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// void _addRemoteParticipantListeners(RemoteParticipant remoteParticipant) {
|
||||
// print('ConferenceRoom._addRemoteParticipantListeners() => Adding listeners to remoteParticipant ${remoteParticipant.sid}');
|
||||
// _streamSubscriptions.add(remoteParticipant.onAudioTrackDisabled.listen(_onAudioTrackDisabled));
|
||||
// _streamSubscriptions.add(remoteParticipant.onAudioTrackEnabled.listen(_onAudioTrackEnabled));
|
||||
// _streamSubscriptions.add(remoteParticipant.onAudioTrackPublished.listen(_onAudioTrackPublished));
|
||||
// _streamSubscriptions.add(remoteParticipant.onAudioTrackSubscribed.listen(_onAudioTrackSubscribed));
|
||||
// _streamSubscriptions.add(remoteParticipant.onAudioTrackSubscriptionFailed.listen(_onAudioTrackSubscriptionFailed));
|
||||
// _streamSubscriptions.add(remoteParticipant.onAudioTrackUnpublished.listen(_onAudioTrackUnpublished));
|
||||
// _streamSubscriptions.add(remoteParticipant.onAudioTrackUnsubscribed.listen(_onAudioTrackUnsubscribed));
|
||||
//
|
||||
// _streamSubscriptions.add(remoteParticipant.onDataTrackPublished.listen(_onDataTrackPublished));
|
||||
// _streamSubscriptions.add(remoteParticipant.onDataTrackSubscribed.listen(_onDataTrackSubscribed));
|
||||
// _streamSubscriptions.add(remoteParticipant.onDataTrackSubscriptionFailed.listen(_onDataTrackSubscriptionFailed));
|
||||
// _streamSubscriptions.add(remoteParticipant.onDataTrackUnpublished.listen(_onDataTrackUnpublished));
|
||||
// _streamSubscriptions.add(remoteParticipant.onDataTrackUnsubscribed.listen(_onDataTrackUnsubscribed));
|
||||
//
|
||||
// _streamSubscriptions.add(remoteParticipant.onVideoTrackDisabled.listen(_onVideoTrackDisabled));
|
||||
// _streamSubscriptions.add(remoteParticipant.onVideoTrackEnabled.listen(_onVideoTrackEnabled));
|
||||
// _streamSubscriptions.add(remoteParticipant.onVideoTrackPublished.listen(_onVideoTrackPublished));
|
||||
// _streamSubscriptions.add(remoteParticipant.onVideoTrackSubscribed.listen(_onVideoTrackSubscribed));
|
||||
// _streamSubscriptions.add(remoteParticipant.onVideoTrackSubscriptionFailed.listen(_onVideoTrackSubscriptionFailed));
|
||||
// _streamSubscriptions.add(remoteParticipant.onVideoTrackUnpublished.listen(_onVideoTrackUnpublished));
|
||||
// _streamSubscriptions.add(remoteParticipant.onVideoTrackUnsubscribed.listen(_onVideoTrackUnsubscribed));
|
||||
// }
|
||||
//
|
||||
// void _onAudioTrackDisabled(RemoteAudioTrackEvent event) {
|
||||
// print('ConferenceRoom._onAudioTrackDisabled(), ${event.remoteParticipant.sid}, ${event.remoteAudioTrackPublication.trackSid}, isEnabled: ${event.remoteAudioTrackPublication.isTrackEnabled}');
|
||||
// _setRemoteAudioEnabled(event);
|
||||
// }
|
||||
//
|
||||
// void _onAudioTrackEnabled(RemoteAudioTrackEvent event) {
|
||||
// print('ConferenceRoom._onAudioTrackEnabled(), ${event.remoteParticipant.sid}, ${event.remoteAudioTrackPublication.trackSid}, isEnabled: ${event.remoteAudioTrackPublication.isTrackEnabled}');
|
||||
// _setRemoteAudioEnabled(event);
|
||||
// }
|
||||
//
|
||||
// void _onAudioTrackPublished(RemoteAudioTrackEvent event) {
|
||||
// print('ConferenceRoom._onAudioTrackPublished(), ${event.remoteParticipant.sid}}');
|
||||
// }
|
||||
//
|
||||
// void _onAudioTrackSubscribed(RemoteAudioTrackSubscriptionEvent event) {
|
||||
// print('ConferenceRoom._onAudioTrackSubscribed(), ${event.remoteParticipant.sid}, ${event.remoteAudioTrackPublication.trackSid}');
|
||||
// _addOrUpdateParticipant(event);
|
||||
// }
|
||||
//
|
||||
// void _onAudioTrackSubscriptionFailed(RemoteAudioTrackSubscriptionFailedEvent event) {
|
||||
// print('ConferenceRoom._onAudioTrackSubscriptionFailed(), ${event.remoteParticipant.sid}, ${event.remoteAudioTrackPublication.trackSid}');
|
||||
// _onExceptionStreamController.add(
|
||||
// PlatformException(
|
||||
// code: 'ConferenceRoom.audioTrackSubscriptionFailed',
|
||||
// message: 'AudioTrack Subscription Failed',
|
||||
// details: event.exception.toString(),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// void _onAudioTrackUnpublished(RemoteAudioTrackEvent event) {
|
||||
// print('ConferenceRoom._onAudioTrackUnpublished(), ${event.remoteParticipant.sid}, ${event.remoteAudioTrackPublication.trackSid}');
|
||||
// }
|
||||
//
|
||||
// void _onAudioTrackUnsubscribed(RemoteAudioTrackSubscriptionEvent event) {
|
||||
// print('ConferenceRoom._onAudioTrackUnsubscribed(), ${event.remoteParticipant.sid}, ${event.remoteAudioTrack.sid}');
|
||||
// }
|
||||
//
|
||||
// void _onDataTrackPublished(RemoteDataTrackEvent event) {
|
||||
// print('ConferenceRoom._onDataTrackPublished(), ${event.remoteParticipant.sid}}');
|
||||
// }
|
||||
//
|
||||
// void _onDataTrackSubscribed(RemoteDataTrackSubscriptionEvent event) {
|
||||
// print('ConferenceRoom._onDataTrackSubscribed(), ${event.remoteParticipant.sid}, ${event.remoteDataTrackPublication.trackSid}');
|
||||
// final dataTrack = event.remoteDataTrackPublication.remoteDataTrack;
|
||||
// _dataTracks.add(dataTrack);
|
||||
// _streamSubscriptions.add(dataTrack.onMessage.listen(_onMessage));
|
||||
// _streamSubscriptions.add(dataTrack.onBufferMessage.listen(_onBufferMessage));
|
||||
// }
|
||||
//
|
||||
// void _onDataTrackSubscriptionFailed(RemoteDataTrackSubscriptionFailedEvent event) {
|
||||
// print('ConferenceRoom._onDataTrackSubscriptionFailed(), ${event.remoteParticipant.sid}, ${event.remoteDataTrackPublication.trackSid}');
|
||||
// _onExceptionStreamController.add(
|
||||
// PlatformException(
|
||||
// code: 'ConferenceRoom.dataTrackSubscriptionFailed',
|
||||
// message: 'DataTrack Subscription Failed',
|
||||
// details: event.exception.toString(),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// void _onDataTrackUnpublished(RemoteDataTrackEvent event) {
|
||||
// print('ConferenceRoom._onDataTrackUnpublished(), ${event.remoteParticipant.sid}, ${event.remoteDataTrackPublication.trackSid}');
|
||||
// }
|
||||
//
|
||||
// void _onDataTrackUnsubscribed(RemoteDataTrackSubscriptionEvent event) {
|
||||
// print('ConferenceRoom._onDataTrackUnsubscribed(), ${event.remoteParticipant.sid}, ${event.remoteDataTrack.sid}');
|
||||
// }
|
||||
//
|
||||
// void _onVideoTrackDisabled(RemoteVideoTrackEvent event) {
|
||||
// print('ConferenceRoom._onVideoTrackDisabled(), ${event.remoteParticipant.sid}, ${event.remoteVideoTrackPublication.trackSid}, isEnabled: ${event.remoteVideoTrackPublication.isTrackEnabled}');
|
||||
// _setRemoteVideoEnabled(event);
|
||||
// }
|
||||
//
|
||||
// void _onVideoTrackEnabled(RemoteVideoTrackEvent event) {
|
||||
// print('ConferenceRoom._onVideoTrackEnabled(), ${event.remoteParticipant.sid}, ${event.remoteVideoTrackPublication.trackSid}, isEnabled: ${event.remoteVideoTrackPublication.isTrackEnabled}');
|
||||
// _setRemoteVideoEnabled(event);
|
||||
// }
|
||||
//
|
||||
// void _onVideoTrackPublished(RemoteVideoTrackEvent event) {
|
||||
// print('ConferenceRoom._onVideoTrackPublished(), ${event.remoteParticipant.sid}, ${event.remoteVideoTrackPublication.trackSid}');
|
||||
// }
|
||||
//
|
||||
// void _onVideoTrackSubscribed(RemoteVideoTrackSubscriptionEvent event) {
|
||||
// print('ConferenceRoom._onVideoTrackSubscribed(), ${event.remoteParticipant.sid}, ${event.remoteVideoTrack.sid}');
|
||||
// _addOrUpdateParticipant(event);
|
||||
// }
|
||||
//
|
||||
// void _onVideoTrackSubscriptionFailed(RemoteVideoTrackSubscriptionFailedEvent event) {
|
||||
// print('ConferenceRoom._onVideoTrackSubscriptionFailed(), ${event.remoteParticipant.sid}, ${event.remoteVideoTrackPublication.trackSid}');
|
||||
// _onExceptionStreamController.add(
|
||||
// PlatformException(
|
||||
// code: 'ConferenceRoom.videoTrackSubscriptionFailed',
|
||||
// message: 'VideoTrack Subscription Failed',
|
||||
// details: event.exception.toString(),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// void _onVideoTrackUnpublished(RemoteVideoTrackEvent event) {
|
||||
// print('ConferenceRoom._onVideoTrackUnpublished(), ${event.remoteParticipant.sid}, ${event.remoteVideoTrackPublication.trackSid}');
|
||||
// }
|
||||
//
|
||||
// void _onVideoTrackUnsubscribed(RemoteVideoTrackSubscriptionEvent event) {
|
||||
// print('ConferenceRoom._onVideoTrackUnsubscribed(), ${event.remoteParticipant.sid}, ${event.remoteVideoTrack.sid}');
|
||||
// }
|
||||
//
|
||||
// void _onMessage(RemoteDataTrackStringMessageEvent event) {
|
||||
// print('onMessage => ${event.remoteDataTrack.sid}, ${event.message}');
|
||||
// }
|
||||
//
|
||||
// void _onBufferMessage(RemoteDataTrackBufferMessageEvent event) {
|
||||
// print('onBufferMessage => ${event.remoteDataTrack.sid}, ${String.fromCharCodes(event.message.asUint8List())}');
|
||||
// }
|
||||
//
|
||||
// void _setRemoteAudioEnabled(RemoteAudioTrackEvent event) {
|
||||
// if (event.remoteAudioTrackPublication == null) {
|
||||
// return;
|
||||
// }
|
||||
// var index = _participants.indexWhere((ParticipantWidget participant) => participant.id == event.remoteParticipant.sid);
|
||||
// if (index < 0) {
|
||||
// return;
|
||||
// }
|
||||
// var participant = _participants[index];
|
||||
// _participants.replaceRange(
|
||||
// index,
|
||||
// index + 1,
|
||||
// [
|
||||
// participant.copyWith(audioEnabled: event.remoteAudioTrackPublication.isTrackEnabled),
|
||||
// ],
|
||||
// );
|
||||
// notifyListeners();
|
||||
// }
|
||||
//
|
||||
// void _setRemoteVideoEnabled(RemoteVideoTrackEvent event) {
|
||||
// if (event.remoteVideoTrackPublication == null) {
|
||||
// return;
|
||||
// }
|
||||
// var index = _participants.indexWhere((ParticipantWidget participant) => participant.id == event.remoteParticipant.sid);
|
||||
// if (index < 0) {
|
||||
// return;
|
||||
// }
|
||||
// var participant = _participants[index];
|
||||
// _participants.replaceRange(
|
||||
// index,
|
||||
// index + 1,
|
||||
// [
|
||||
// participant.copyWith(videoEnabled: event.remoteVideoTrackPublication.isTrackEnabled),
|
||||
// ],
|
||||
// );
|
||||
// notifyListeners();
|
||||
// }
|
||||
//
|
||||
// void _addOrUpdateParticipant(RemoteParticipantEvent event) {
|
||||
// print('ConferenceRoom._addOrUpdateParticipant(), ${event.remoteParticipant.sid}');
|
||||
// final participant = _participants.firstWhere(
|
||||
// (ParticipantWidget participant) => participant.id == event.remoteParticipant.sid,
|
||||
// orElse: () => null,
|
||||
// );
|
||||
// if (participant != null) {
|
||||
// print('Participant found: ${participant.id}, updating A/V enabled values');
|
||||
// _setRemoteVideoEnabled(event);
|
||||
// _setRemoteAudioEnabled(event);
|
||||
// } else {
|
||||
// final bufferedParticipant = _participantBuffer.firstWhere(
|
||||
// (ParticipantBuffer participant) => participant.id == event.remoteParticipant.sid,
|
||||
// orElse: () => null,
|
||||
// );
|
||||
// if (bufferedParticipant != null) {
|
||||
// _participantBuffer.remove(bufferedParticipant);
|
||||
// } else if (event is RemoteAudioTrackEvent) {
|
||||
// print('Audio subscription came first, waiting for the video subscription...');
|
||||
// _participantBuffer.add(
|
||||
// ParticipantBuffer(
|
||||
// id: event.remoteParticipant.sid,
|
||||
// audioEnabled: event.remoteAudioTrackPublication?.remoteAudioTrack?.isEnabled ?? true,
|
||||
// ),
|
||||
// );
|
||||
// return;
|
||||
// }
|
||||
// if (event is RemoteVideoTrackSubscriptionEvent) {
|
||||
// print('New participant, adding: ${event.remoteParticipant.sid}');
|
||||
// _participants.insert(
|
||||
// 0,
|
||||
// _buildParticipant(
|
||||
// child: event.remoteVideoTrack.widget(),
|
||||
// id: event.remoteParticipant.sid,
|
||||
// remoteParticipant: event.remoteParticipant,
|
||||
// audioEnabled: bufferedParticipant?.audioEnabled ?? true,
|
||||
// videoEnabled: event.remoteVideoTrackPublication?.remoteVideoTrack?.isEnabled ?? true,
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// notifyListeners();
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
@ -0,0 +1,172 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:doctor_app_flutter/screens/live_care/web-rtc/clipped_video.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class DraggablePublisher extends StatefulWidget {
|
||||
final Size availableScreenSize;
|
||||
final Widget child;
|
||||
final double scaleFactor;
|
||||
final Stream<bool> onButtonBarVisible;
|
||||
final Stream<double> onButtonBarHeight;
|
||||
|
||||
const DraggablePublisher({
|
||||
Key? key,
|
||||
required this.availableScreenSize,
|
||||
required this.child,
|
||||
required this.onButtonBarVisible,
|
||||
required this.onButtonBarHeight,
|
||||
|
||||
/// The portion of the screen the DraggableWidget should use.
|
||||
this.scaleFactor = .25,
|
||||
}) : assert(scaleFactor != null && scaleFactor > 0 && scaleFactor <= .4),
|
||||
assert(availableScreenSize != null),
|
||||
assert(onButtonBarVisible != null),
|
||||
assert(onButtonBarHeight != null),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
_DraggablePublisherState createState() => _DraggablePublisherState();
|
||||
}
|
||||
|
||||
class _DraggablePublisherState extends State<DraggablePublisher> {
|
||||
bool _isButtonBarVisible = true;
|
||||
double _buttonBarHeight = 0;
|
||||
late double _width;
|
||||
late double _height;
|
||||
late double _top;
|
||||
late double _left;
|
||||
late double _viewPaddingTop;
|
||||
late double _viewPaddingBottom;
|
||||
final double _padding = 8.0;
|
||||
final Duration _duration300ms = const Duration(milliseconds: 300);
|
||||
final Duration _duration0ms = const Duration(milliseconds: 0);
|
||||
late Duration _duration;
|
||||
late StreamSubscription _streamSubscription;
|
||||
late StreamSubscription _streamHeightSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_duration = _duration300ms;
|
||||
_width = widget.availableScreenSize.width * widget.scaleFactor;
|
||||
_height = _width * (widget.availableScreenSize.height / widget.availableScreenSize.width);
|
||||
_top = widget.availableScreenSize.height - (_buttonBarHeight + _padding) - _height;
|
||||
_left = widget.availableScreenSize.width - _padding - _width;
|
||||
|
||||
_streamSubscription = widget.onButtonBarVisible.listen(_buttonBarVisible);
|
||||
_streamHeightSubscription = widget.onButtonBarHeight.listen(_getButtonBarHeight);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
final mediaQuery = MediaQuery.of(context);
|
||||
_viewPaddingTop = mediaQuery.viewPadding.top;
|
||||
_viewPaddingBottom = mediaQuery.viewPadding.bottom;
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_streamSubscription.cancel();
|
||||
_streamHeightSubscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _getButtonBarHeight(double height) {
|
||||
setState(() {
|
||||
_buttonBarHeight = height;
|
||||
_positionWidget();
|
||||
});
|
||||
}
|
||||
|
||||
void _buttonBarVisible(bool visible) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_isButtonBarVisible = visible;
|
||||
if (_duration == _duration300ms) {
|
||||
// only position the widget when we are not currently dragging it around
|
||||
_positionWidget();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedPositioned(
|
||||
top: _top,
|
||||
left: _left,
|
||||
width: _width,
|
||||
height: _height,
|
||||
duration: _duration,
|
||||
child: Listener(
|
||||
onPointerDown: (_) => _duration = _duration0ms,
|
||||
onPointerMove: (PointerMoveEvent event) {
|
||||
setState(() {
|
||||
_left = (_left + event.delta.dx).roundToDouble();
|
||||
_top = (_top + event.delta.dy).roundToDouble();
|
||||
});
|
||||
},
|
||||
onPointerUp: (_) => _positionWidget(),
|
||||
onPointerCancel: (_) => _positionWidget(),
|
||||
child: ClippedVideo(
|
||||
height: _height,
|
||||
width: _width,
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
double _getCurrentStatusBarHeight() {
|
||||
if (_isButtonBarVisible) {
|
||||
return _viewPaddingTop;
|
||||
}
|
||||
final _defaultViewPaddingTop = Platform.isIOS ? 20.0 : Platform.isAndroid ? 24.0 : 0.0;
|
||||
if (_viewPaddingTop > _defaultViewPaddingTop) {
|
||||
// There must be a hardware notch in the display.
|
||||
return _viewPaddingTop;
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
double _getCurrentButtonBarHeight() {
|
||||
if (_isButtonBarVisible) {
|
||||
return _buttonBarHeight + _viewPaddingBottom;
|
||||
}
|
||||
return _viewPaddingBottom;
|
||||
}
|
||||
|
||||
void _positionWidget() {
|
||||
// Determine the center of the object being dragged so we can decide
|
||||
// in which corner the object should be placed.
|
||||
var dx = (_width / 2) + _left;
|
||||
dx = dx < 0 ? 0 : dx >= widget.availableScreenSize.width ? widget.availableScreenSize.width - 1 : dx;
|
||||
var dy = (_height / 2) + _top;
|
||||
dy = dy < 0 ? 0 : dy >= widget.availableScreenSize.height ? widget.availableScreenSize.height - 1 : dy;
|
||||
final draggableCenter = Offset(dx, dy);
|
||||
|
||||
setState(() {
|
||||
_duration = _duration300ms;
|
||||
if (Rect.fromLTRB(0, 0, widget.availableScreenSize.width / 2, widget.availableScreenSize.height / 2).contains(draggableCenter)) {
|
||||
// Top-left
|
||||
_top = _getCurrentStatusBarHeight() + _padding;
|
||||
_left = _padding;
|
||||
} else if (Rect.fromLTRB(widget.availableScreenSize.width / 2, 0, widget.availableScreenSize.width, widget.availableScreenSize.height / 2).contains(draggableCenter)) {
|
||||
// Top-right
|
||||
_top = _getCurrentStatusBarHeight() + _padding;
|
||||
_left = widget.availableScreenSize.width - _padding - _width;
|
||||
} else if (Rect.fromLTRB(0, widget.availableScreenSize.height / 2, widget.availableScreenSize.width / 2, widget.availableScreenSize.height).contains(draggableCenter)) {
|
||||
// Bottom-left
|
||||
_top = widget.availableScreenSize.height - (_getCurrentButtonBarHeight() + _padding) - _height;
|
||||
_left = _padding;
|
||||
} else if (Rect.fromLTRB(widget.availableScreenSize.width / 2, widget.availableScreenSize.height / 2, widget.availableScreenSize.width, widget.availableScreenSize.height).contains(draggableCenter)) {
|
||||
// Bottom-right
|
||||
_top = widget.availableScreenSize.height - (_getCurrentButtonBarHeight() + _padding) - _height;
|
||||
_left = widget.availableScreenSize.width - _padding - _width;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,197 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ParticipantBuffer {
|
||||
final bool audioEnabled;
|
||||
final String id;
|
||||
|
||||
ParticipantBuffer({
|
||||
required this.audioEnabled,
|
||||
required this.id,
|
||||
}) : assert(audioEnabled != null),
|
||||
assert(id != null);
|
||||
}
|
||||
|
||||
class ParticipantWidget extends StatelessWidget {
|
||||
final Widget child;
|
||||
final String id;
|
||||
final bool audioEnabled;
|
||||
final bool videoEnabled;
|
||||
final bool isRemote;
|
||||
final bool isDummy;
|
||||
final bool isDominant;
|
||||
|
||||
const ParticipantWidget({
|
||||
Key? key,
|
||||
required this.child,
|
||||
required this.audioEnabled,
|
||||
required this.videoEnabled,
|
||||
required this.id,
|
||||
required this.isRemote,
|
||||
this.isDominant = false,
|
||||
this.isDummy = false,
|
||||
}) : assert(child != null),
|
||||
assert(audioEnabled != null),
|
||||
assert(videoEnabled != null),
|
||||
assert(isRemote != null),
|
||||
assert(isDominant != null),
|
||||
assert(isDummy != null),
|
||||
super(key: key);
|
||||
|
||||
ParticipantWidget copyWith({
|
||||
Widget? child,
|
||||
bool? audioEnabled,
|
||||
bool? videoEnabled,
|
||||
bool? isDominant,
|
||||
}) {
|
||||
return ParticipantWidget(
|
||||
id: id,
|
||||
child: child ?? this.child,
|
||||
audioEnabled: audioEnabled ?? this.audioEnabled,
|
||||
videoEnabled: videoEnabled ?? this.videoEnabled,
|
||||
isDominant: isDominant ?? this.isDominant,
|
||||
isRemote: isRemote,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final children = <Widget>[];
|
||||
final icons = <Widget>[];
|
||||
if (!videoEnabled) {
|
||||
icons.add(_buildVideoEnabledIcon());
|
||||
children.add(
|
||||
ClipRect(
|
||||
// Need to clip this BackdropFilter, otherwise it will blur the entire screen
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(color: Colors.black.withOpacity(.1)),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
children.add(child);
|
||||
}
|
||||
children.add(Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: AnimatedOpacity(
|
||||
duration: Duration(milliseconds: 500),
|
||||
opacity: isDominant ? 1 : 0,
|
||||
child: Icon(
|
||||
Icons.volume_up,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
));
|
||||
if (!audioEnabled) {
|
||||
icons.add(_buildAudioEnabledIcon());
|
||||
}
|
||||
if (icons.isNotEmpty) {
|
||||
if (isRemote) {
|
||||
final rows = <Widget>[];
|
||||
rows.add(_buildRow(icons));
|
||||
if (!audioEnabled && !videoEnabled) {
|
||||
rows.add(_buildRow(_fitText('The camera and microphone are off', Colors.white24)));
|
||||
} else if (!audioEnabled) {
|
||||
rows.add(_buildRow(_fitText('The microphone is off', Colors.black26)));
|
||||
} else if (!videoEnabled) {
|
||||
rows.add(_buildRow(_fitText('The camera is off', Colors.white24)));
|
||||
}
|
||||
children.add(
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: rows,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
children.add(Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: icons,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return Stack(
|
||||
children: children,
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _fitText(String text, Color color) {
|
||||
return [
|
||||
Flexible(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0, right: 8.0),
|
||||
child: Text(text, maxLines: 1, style: _buildTextStyle(color)),
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
TextStyle _buildTextStyle(Color color) {
|
||||
return TextStyle(
|
||||
color: color,
|
||||
shadows: <Shadow>[
|
||||
Shadow(
|
||||
blurRadius: 1.0,
|
||||
color: Color.fromARGB(255, 0, 0, 0),
|
||||
),
|
||||
Shadow(
|
||||
blurRadius: 1.0,
|
||||
color: Color.fromARGB(24, 255, 255, 255),
|
||||
),
|
||||
],
|
||||
fontSize: 15,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRow(List<Widget> children) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: children,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAudioEnabledIcon() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: CircleAvatar(
|
||||
maxRadius: 15,
|
||||
child: FittedBox(
|
||||
child: Icon(
|
||||
Icons.mic_off,
|
||||
color: Colors.black,
|
||||
key: Key('microphone-off-icon'),
|
||||
),
|
||||
),
|
||||
backgroundColor: Colors.white24,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVideoEnabledIcon() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: CircleAvatar(
|
||||
maxRadius: 15,
|
||||
child: FittedBox(
|
||||
child: Icon(
|
||||
Icons.videocam_off,
|
||||
color: Colors.black,
|
||||
key: Key('videocam-off-icon'),
|
||||
),
|
||||
),
|
||||
backgroundColor: Colors.white24,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,231 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:doctor_app_flutter/util/SignalRUtil.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||
|
||||
const PATIENT_PUSH_TOKEN = "eUA6FUiOTbSellej2JQ8yg:APA91bFuDledDvwbeZJ8KU8drHQ7kse7h9UjKPaBZsQgKi-0GCSPRB_yIO9O3PXysWLnLMJYwVmEJWd-jPIqbrLz_Z_yzE--3mC-GZJj92BT0nSzAVd-JVr28pUtejgSjTpZryNFlSn5";
|
||||
|
||||
typedef void StreamStateCallback(MediaStream stream);
|
||||
typedef void RTCIceGatheringStateCallback(RTCIceGatheringState state);
|
||||
typedef void RTCPeerConnectionStateCallback(RTCPeerConnectionState state);
|
||||
typedef void RTCSignalingStateCallback(RTCSignalingState state);
|
||||
|
||||
|
||||
|
||||
|
||||
Map<String, dynamic> snapsis_ice_config = {
|
||||
'iceServers': [
|
||||
{ "urls": 'stun:15.185.116.59:3478' },
|
||||
{ "urls": "turn:15.185.116.59:3479", "username": "admin", "credential": "admin" },
|
||||
],
|
||||
// 'sdpSemantics': 'unified-plan'
|
||||
};
|
||||
Map<String, dynamic> twilio_ice_config = {
|
||||
"ice_servers": [
|
||||
{
|
||||
"url": "stun:global.stun.twilio.com:3478?transport=udp",
|
||||
"urls": "stun:global.stun.twilio.com:3478?transport=udp"
|
||||
},
|
||||
{
|
||||
"url": "turn:global.turn.twilio.com:3478?transport=udp",
|
||||
"username": "ce8042842b62c21bd20b176f80d6067fd3db81b1e9766312418ef5421d9ca2a2",
|
||||
"urls": "turn:global.turn.twilio.com:3478?transport=udp",
|
||||
"credential": "UzGOsiLwPZJ32cjafAebfDDpVrqeQjgpFHZEdau/8r4="
|
||||
},
|
||||
{
|
||||
"url": "turn:global.turn.twilio.com:3478?transport=tcp",
|
||||
"username": "ce8042842b62c21bd20b176f80d6067fd3db81b1e9766312418ef5421d9ca2a2",
|
||||
"urls": "turn:global.turn.twilio.com:3478?transport=tcp",
|
||||
"credential": "UzGOsiLwPZJ32cjafAebfDDpVrqeQjgpFHZEdau/8r4="
|
||||
},
|
||||
{
|
||||
"url": "turn:global.turn.twilio.com:443?transport=tcp",
|
||||
"username": "ce8042842b62c21bd20b176f80d6067fd3db81b1e9766312418ef5421d9ca2a2",
|
||||
"urls": "turn:global.turn.twilio.com:443?transport=tcp",
|
||||
"credential": "UzGOsiLwPZJ32cjafAebfDDpVrqeQjgpFHZEdau/8r4="
|
||||
}
|
||||
],
|
||||
// 'sdpSemantics': 'unified-plan'
|
||||
};
|
||||
Map<String, dynamic> google_ice_config = {
|
||||
'iceServers': [
|
||||
{
|
||||
'urls': [
|
||||
'stun:stun.l.google.com:19302',
|
||||
'stun:stun1.l.google.com:19302',
|
||||
'stun:stun2.l.google.com:19302',
|
||||
'stun:stun3.l.google.com:19302'
|
||||
]
|
||||
},
|
||||
],
|
||||
// 'sdpSemantics': 'unified-plan'
|
||||
};
|
||||
Map<String, dynamic> aws_ice_config = {
|
||||
'iceServers': [
|
||||
{'url': "stun:ec2-15-185-116-59.me-south-1.compute.amazonaws.com:3478"},
|
||||
{'url': "turn:ec2-15-185-116-59.me-south-1.compute.amazonaws.com:3479", 'credential': "admin", 'username': "admin"}
|
||||
],
|
||||
// 'sdpSemantics': 'unified-plan'
|
||||
};
|
||||
|
||||
final Map<String, dynamic> _peer_config = {
|
||||
'mandatory': {},
|
||||
'optional': [
|
||||
{'DtlsSrtpKeyAgreement': true},
|
||||
]
|
||||
};
|
||||
|
||||
class Signaling {
|
||||
|
||||
dispose(){
|
||||
if(peerConnection != null)
|
||||
peerConnection.dispose();
|
||||
signalR?.closeConnection();
|
||||
}
|
||||
|
||||
init(){
|
||||
// Create Peer Connection
|
||||
createPeerConnection(google_ice_config,_peer_config).then((value){
|
||||
peerConnection = value;
|
||||
registerPeerConnectionListeners();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
initializeSignalR(String userName) async{
|
||||
if(signalR != null)
|
||||
await signalR?.closeConnection();
|
||||
// https://vcallapi.hmg.com/webRTCHub?source=web&username=zohaib
|
||||
signalR = SignalRUtil(hubName: "https://vcallapi.hmg.com/webRTCHub?source=mobile&username=$userName");
|
||||
final connected = await signalR?.openConnection();
|
||||
if(connected != null && !connected)
|
||||
throw 'Failed to connect SignalR';
|
||||
}
|
||||
|
||||
SignalRUtil? signalR;
|
||||
|
||||
late RTCPeerConnection peerConnection;
|
||||
MediaStream? localStream;
|
||||
MediaStream? remoteStream;
|
||||
RTCDataChannel? dataChannel;
|
||||
|
||||
Future<bool> acceptCall(String caller, String receiver, {required MediaStream localMediaStream, required Function(MediaStream) onRemoteMediaStream}) async{
|
||||
await initializeSignalR(caller);
|
||||
signalR?.setContributors(caller: caller, receiver: receiver);
|
||||
await signalR?.acceptCall(receiver, caller).catchError((e) => throw 'Failed to inform signalR that i accepted a call');
|
||||
|
||||
peerConnection.addStream(localMediaStream);
|
||||
|
||||
peerConnection.onAddStream = (MediaStream stream) {
|
||||
remoteStream = stream;
|
||||
onRemoteMediaStream.call(stream);
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
Future<bool> initiateCall(String caller, String receiver, {required MediaStream localMediaStream, required Function(MediaStream) onRemoteMediaStream}) async{
|
||||
await initializeSignalR(caller);
|
||||
signalR?.setContributors(caller: caller, receiver: receiver);
|
||||
await signalR?.callUserMobile(caller, receiver).catchError((e) => throw 'Failed to inform signalR to call user: $receiver');
|
||||
|
||||
peerConnection.addStream(localMediaStream);
|
||||
|
||||
peerConnection.onAddStream = (MediaStream stream) {
|
||||
remoteStream = stream;
|
||||
onRemoteMediaStream.call(stream);
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Future hangupCall(String caller, String receiver) async{
|
||||
await signalR?.hangupCall(caller, receiver);
|
||||
dispose();
|
||||
}
|
||||
|
||||
answerOffer(Map offerSdp) async{
|
||||
final caller = offerSdp['caller'];
|
||||
final receiver = offerSdp['target'];
|
||||
peerConnection.setRemoteDescription(rtcSessionDescriptionFrom(offerSdp))
|
||||
.then((value) {
|
||||
return peerConnection.createAnswer();
|
||||
})
|
||||
.then((anwser) {
|
||||
return peerConnection.setLocalDescription(anwser);
|
||||
})
|
||||
.then((value) {
|
||||
return peerConnection.getLocalDescription();
|
||||
})
|
||||
.then((answer) {
|
||||
return signalR?.answerOffer(answer, caller, receiver);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
sdpOfferAnswered(Map sdp){
|
||||
final answerSdp = rtcSessionDescriptionFrom(sdp);
|
||||
peerConnection.setRemoteDescription(answerSdp)
|
||||
.then((value) {
|
||||
}).catchError((e) => print(e));
|
||||
}
|
||||
|
||||
Future<void> hangUp(RTCVideoRenderer localVideo) async {
|
||||
|
||||
}
|
||||
|
||||
Future<String> createSdpAnswer(String toOfferSdp) async {
|
||||
final offerSdp = rtcSessionDescriptionFrom(jsonDecode(toOfferSdp));
|
||||
peerConnection.setRemoteDescription(offerSdp);
|
||||
|
||||
final answer = await peerConnection.createAnswer();
|
||||
var answerSdp = json.encode(answer); // Send SDP via Push or any channel
|
||||
return answerSdp;
|
||||
}
|
||||
|
||||
Future<String> createSdpOffer(String caller, String receiver) async {
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
final offer = await peerConnection.createOffer();
|
||||
await peerConnection.setLocalDescription(offer);
|
||||
final map = offer.toMap();
|
||||
var offerSdp = json.encode({'sdp':map, 'target':receiver, 'caller':caller}); // Send SDP via Push or any channel
|
||||
return offerSdp;
|
||||
}
|
||||
|
||||
addCandidate(Map candidate){
|
||||
peerConnection.addCandidate(rtcIceCandidateFrom(candidate));
|
||||
}
|
||||
|
||||
void registerPeerConnectionListeners() {
|
||||
peerConnection.onIceCandidate = (RTCIceCandidate candidate){
|
||||
print(json.encode(candidate.toMap()));
|
||||
signalR?.addIceCandidate(candidate);
|
||||
};
|
||||
|
||||
peerConnection.onIceGatheringState = (RTCIceGatheringState state) {
|
||||
print('ICE gathering state changed: $state');
|
||||
};
|
||||
|
||||
peerConnection.onConnectionState = (RTCPeerConnectionState state) {
|
||||
print('Connection state change: $state ${state.index}');
|
||||
};
|
||||
|
||||
peerConnection.onSignalingState = (RTCSignalingState state) {
|
||||
print('Signaling state change: $state');
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
rtcSessionDescriptionFrom(Map sdp){
|
||||
return RTCSessionDescription(
|
||||
sdp['sdp'],sdp['type'],
|
||||
);
|
||||
}
|
||||
|
||||
rtcIceCandidateFrom(Map candidate){
|
||||
final _candidate = candidate['candidate'];
|
||||
return RTCIceCandidate(_candidate['candidate'], _candidate['sdpMid'], _candidate['sdpMLineIndex']);
|
||||
}
|
||||
@ -0,0 +1,198 @@
|
||||
import 'dart:async';
|
||||
import 'package:doctor_app_flutter/screens/live_care/web-rtc/signaling.dart';
|
||||
import 'package:doctor_app_flutter/screens/live_care/web-rtc/web_rtc/widgets/cam_view_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||
|
||||
import '../conference_button_bar.dart';
|
||||
|
||||
class CallHomePage extends StatefulWidget {
|
||||
final String receiverId;
|
||||
final String callerId;
|
||||
|
||||
const CallHomePage({Key? key, required this.receiverId, required this.callerId}) : super(key: key);
|
||||
|
||||
@override
|
||||
_CallHomePageState createState() => _CallHomePageState();
|
||||
}
|
||||
|
||||
class _CallHomePageState extends State<CallHomePage> {
|
||||
|
||||
String get caller => widget.callerId;
|
||||
String get receiver => widget.receiverId;
|
||||
|
||||
bool showNoise = true;
|
||||
RTCVideoRenderer _localRenderer = RTCVideoRenderer();
|
||||
RTCVideoRenderer _remoteRenderer = RTCVideoRenderer();
|
||||
|
||||
final StreamController<bool> _audioButton = StreamController<bool>.broadcast();
|
||||
final StreamController<bool> _videoButton = StreamController<bool>.broadcast();
|
||||
final StreamController<bool> _onButtonBarVisibleStreamController = StreamController<bool>.broadcast();
|
||||
final StreamController<double> _onButtonBarHeightStreamController = StreamController<double>.broadcast();
|
||||
|
||||
//Stream to enable video
|
||||
late MediaStream localMediaStream;
|
||||
MediaStream? remoteMediaStream;
|
||||
Signaling signaling = Signaling()..init();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
// TODO: implement initState
|
||||
super.initState();
|
||||
startCall();
|
||||
}
|
||||
|
||||
startCall() async{
|
||||
await _localRenderer.initialize();
|
||||
await _remoteRenderer.initialize();
|
||||
final connected = await callUser();
|
||||
}
|
||||
|
||||
|
||||
Future<bool> callUser() async {
|
||||
//Stream local media
|
||||
localMediaStream = await navigator.mediaDevices.getUserMedia({'video': true, 'audio': true});
|
||||
_localRenderer.srcObject = localMediaStream;
|
||||
|
||||
final connected = await signaling.initiateCall(widget.callerId, widget.receiverId, localMediaStream: localMediaStream, onRemoteMediaStream: (remoteMediaStream){
|
||||
setState(() {
|
||||
this.remoteMediaStream = remoteMediaStream;
|
||||
_remoteRenderer.srcObject = remoteMediaStream;
|
||||
});
|
||||
});
|
||||
|
||||
if(connected){
|
||||
signaling.signalR?.listen(
|
||||
onAcceptCall: (arg0) async{
|
||||
final offer = await signaling.createSdpOffer(caller, receiver);
|
||||
signaling.signalR?.offer(receiver, caller, offer).catchError((e){
|
||||
print(e.toString());
|
||||
});
|
||||
},
|
||||
onCandidate: (candidateJson){
|
||||
signaling.addCandidate(candidateJson);
|
||||
},
|
||||
onDeclineCall: (arg0,arg1){
|
||||
// _onHangup();
|
||||
},
|
||||
onHangupCall: (arg0){
|
||||
// _onHangup();
|
||||
},
|
||||
onSdpAnswer: (answerSdp){
|
||||
signaling.sdpOfferAnswered(answerSdp);
|
||||
},
|
||||
onSdpOffer: (offerSdp, user) async{
|
||||
print('${offerSdp.toString()} | ${user.toString()}');
|
||||
await signaling.answerOffer(offerSdp);
|
||||
}
|
||||
);
|
||||
}
|
||||
return connected;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// TODO: implement dispose
|
||||
super.dispose();
|
||||
_localRenderer.dispose();
|
||||
_remoteRenderer.dispose();
|
||||
_audioButton.close();
|
||||
_videoButton.close();
|
||||
localMediaStream?.dispose();
|
||||
remoteMediaStream?.dispose();
|
||||
_disposeStreamsAndSubscriptions();
|
||||
}
|
||||
|
||||
Future<void> _disposeStreamsAndSubscriptions() async {
|
||||
if (_onButtonBarVisibleStreamController != null) await _onButtonBarVisibleStreamController.close();
|
||||
if (_onButtonBarHeightStreamController != null) await _onButtonBarHeightStreamController.close();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: buildLayout(),
|
||||
);
|
||||
}
|
||||
|
||||
LayoutBuilder buildLayout() {
|
||||
return LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
return Stack(
|
||||
children: [
|
||||
CamViewWidget(
|
||||
localRenderer: _localRenderer,
|
||||
remoteRenderer: _remoteRenderer,
|
||||
constraints: constraints,
|
||||
onButtonBarVisibleStreamController: _onButtonBarVisibleStreamController,
|
||||
onButtonBarHeightStreamController: _onButtonBarHeightStreamController,
|
||||
),
|
||||
ConferenceButtonBar(
|
||||
audioEnabled: _audioButton.stream,
|
||||
videoEnabled: _videoButton.stream,
|
||||
onAudioEnabled: _onAudioEnable,
|
||||
onVideoEnabled: _onVideoEnabled,
|
||||
onSwitchCamera: _onSwitchCamera,
|
||||
onHangup: _onHangup,
|
||||
onPersonAdd: () {},
|
||||
onPersonRemove: () {},
|
||||
onHeight: _onHeightBar,
|
||||
onShow: _onShowBar,
|
||||
onHide: _onHideBar,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
_onAudioEnable() {
|
||||
final audioTrack = localMediaStream?.getAudioTracks()[0];
|
||||
if(audioTrack != null){
|
||||
final mute = audioTrack.muted ?? false;
|
||||
Helper.setMicrophoneMute(mute, audioTrack);
|
||||
_audioButton.add(mute);
|
||||
}
|
||||
}
|
||||
|
||||
_onVideoEnabled() {
|
||||
final videoTrack = localMediaStream?.getVideoTracks()[0];
|
||||
if(videoTrack != null){
|
||||
bool videoEnabled = videoTrack.enabled;
|
||||
localMediaStream?.getVideoTracks()[0].enabled = !videoEnabled;
|
||||
_videoButton.add(!videoEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
_onSwitchCamera() {
|
||||
Helper.switchCamera(localMediaStream.getVideoTracks()[0]);
|
||||
}
|
||||
|
||||
void _onShowBar() {
|
||||
setState(() {
|
||||
});
|
||||
_onButtonBarVisibleStreamController.add(true);
|
||||
}
|
||||
|
||||
void _onHeightBar(double height) {
|
||||
_onButtonBarHeightStreamController.add(height);
|
||||
}
|
||||
|
||||
void _onHideBar() {
|
||||
setState(() {
|
||||
SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.bottom]);
|
||||
});
|
||||
_onButtonBarVisibleStreamController.add(false);
|
||||
}
|
||||
|
||||
Future<void> _onHangup() async {
|
||||
signaling.hangupCall(widget.callerId, widget.receiverId);
|
||||
signaling.dispose();
|
||||
print('onHangup');
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -0,0 +1,82 @@
|
||||
import 'dart:async';
|
||||
import 'dart:core';
|
||||
|
||||
import 'package:doctor_app_flutter/screens/live_care/web-rtc/widgets/noise_box.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||
|
||||
import 'draggable_cam.dart';
|
||||
|
||||
class CamViewWidget extends StatefulWidget {
|
||||
RTCVideoRenderer localRenderer;
|
||||
RTCVideoRenderer remoteRenderer;
|
||||
MediaStream? localStream;
|
||||
BoxConstraints constraints;
|
||||
StreamController<bool> onButtonBarVisibleStreamController;
|
||||
StreamController<double> onButtonBarHeightStreamController;
|
||||
|
||||
CamViewWidget({required this.localRenderer, required this.remoteRenderer, required this.constraints, required this.onButtonBarVisibleStreamController, required this.onButtonBarHeightStreamController});
|
||||
|
||||
@override
|
||||
_CamViewWidgetState createState() => _CamViewWidgetState();
|
||||
}
|
||||
|
||||
class _CamViewWidgetState extends State<CamViewWidget> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
child: Stack(
|
||||
children: [
|
||||
FractionallySizedBox(
|
||||
heightFactor: 1, widthFactor: 1,
|
||||
child: Container(
|
||||
color: Colors.black87,
|
||||
child: RTCVideoView(widget.remoteRenderer, mirror: true,filterQuality: FilterQuality.medium,),
|
||||
),
|
||||
),
|
||||
|
||||
if(widget.remoteRenderer.srcObject == null)
|
||||
Positioned.fill(child: _buildNoiseBox()),
|
||||
|
||||
Positioned.fill(
|
||||
child: RTCVideoView(widget.remoteRenderer)
|
||||
),
|
||||
|
||||
DraggableCam(
|
||||
key: Key('publisher'),
|
||||
onButtonBarHeight: widget.onButtonBarHeightStreamController.stream,
|
||||
onButtonBarVisible: widget.onButtonBarVisibleStreamController.stream,
|
||||
availableScreenSize: widget.constraints.biggest,
|
||||
child: RTCVideoView(widget.localRenderer)
|
||||
),
|
||||
|
||||
if(widget.remoteRenderer.srcObject == null)
|
||||
Container(
|
||||
margin: EdgeInsets.all(MediaQuery.of(context).size.width/8),
|
||||
child: Text(
|
||||
'Waiting for another participant to connect to the call...',
|
||||
key: Key('text-wait'),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: Colors.white),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Widget _buildNoiseBox() {
|
||||
return NoiseBox(
|
||||
density: NoiseBoxDensity.xHigh,
|
||||
backgroundColor: Colors.grey.shade900,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,173 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:doctor_app_flutter/screens/live_care/web-rtc/clipped_video.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class DraggableCam extends StatefulWidget {
|
||||
final Size availableScreenSize;
|
||||
final Widget child;
|
||||
final double scaleFactor;
|
||||
final Stream<bool> onButtonBarVisible;
|
||||
final Stream<double> onButtonBarHeight;
|
||||
|
||||
const DraggableCam({
|
||||
Key? key,
|
||||
required this.availableScreenSize,
|
||||
required this.child,
|
||||
required this.onButtonBarVisible,
|
||||
required this.onButtonBarHeight,
|
||||
|
||||
/// The portion of the screen the DraggableWidget should use.
|
||||
this.scaleFactor = .25,
|
||||
}) : assert(scaleFactor != null && scaleFactor > 0 && scaleFactor <= .4),
|
||||
assert(availableScreenSize != null),
|
||||
assert(onButtonBarVisible != null),
|
||||
assert(onButtonBarHeight != null),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
_DraggablePublisherState createState() => _DraggablePublisherState();
|
||||
}
|
||||
|
||||
class _DraggablePublisherState extends State<DraggableCam> {
|
||||
bool _isButtonBarVisible = true;
|
||||
double _buttonBarHeight = 0;
|
||||
late double _width;
|
||||
late double _height;
|
||||
late double _top;
|
||||
late double _left;
|
||||
late double _viewPaddingTop;
|
||||
late double _viewPaddingBottom;
|
||||
final double _padding = 8.0;
|
||||
final Duration _duration300ms = const Duration(milliseconds: 300);
|
||||
final Duration _duration0ms = const Duration(milliseconds: 0);
|
||||
late Duration _duration;
|
||||
late StreamSubscription _streamSubscription;
|
||||
late StreamSubscription _streamHeightSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_duration = _duration300ms;
|
||||
_width = widget.availableScreenSize.width * widget.scaleFactor;
|
||||
_height = _width * (widget.availableScreenSize.height / widget.availableScreenSize.width);
|
||||
_top = widget.availableScreenSize.height - (_buttonBarHeight + _padding) - _height;
|
||||
_left = widget.availableScreenSize.width - _padding - _width;
|
||||
|
||||
_streamSubscription = widget.onButtonBarVisible.listen(_buttonBarVisible);
|
||||
_streamHeightSubscription = widget.onButtonBarHeight.listen(_getButtonBarHeight);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
final mediaQuery = MediaQuery.of(context);
|
||||
_viewPaddingTop = mediaQuery.viewPadding.top;
|
||||
_viewPaddingBottom = mediaQuery.viewPadding.bottom;
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_streamSubscription.cancel();
|
||||
_streamHeightSubscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _getButtonBarHeight(double height) {
|
||||
setState(() {
|
||||
_buttonBarHeight = height;
|
||||
_positionWidget();
|
||||
});
|
||||
}
|
||||
|
||||
void _buttonBarVisible(bool visible) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_isButtonBarVisible = visible;
|
||||
if (_duration == _duration300ms) {
|
||||
// only position the widget when we are not currently dragging it around
|
||||
_positionWidget();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedPositioned(
|
||||
top: _top,
|
||||
left: _left,
|
||||
width: _width,
|
||||
height: _height,
|
||||
duration: _duration,
|
||||
child: Listener(
|
||||
onPointerDown: (_) => _duration = _duration0ms,
|
||||
onPointerMove: (PointerMoveEvent event) {
|
||||
setState(() {
|
||||
_left = (_left + event.delta.dx).roundToDouble();
|
||||
_top = (_top + event.delta.dy).roundToDouble();
|
||||
});
|
||||
},
|
||||
onPointerUp: (_) => _positionWidget(),
|
||||
onPointerCancel: (_) => _positionWidget(),
|
||||
child: ClippedVideo(
|
||||
height: _height,
|
||||
width: _width,
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
double _getCurrentStatusBarHeight() {
|
||||
if (_isButtonBarVisible) {
|
||||
return _viewPaddingTop;
|
||||
}
|
||||
final _defaultViewPaddingTop = Platform.isIOS ? 20.0 : Platform.isAndroid ? 24.0 : 0.0;
|
||||
if (_viewPaddingTop > _defaultViewPaddingTop) {
|
||||
// There must be a hardware notch in the display.
|
||||
return _viewPaddingTop;
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
double _getCurrentButtonBarHeight() {
|
||||
if (_isButtonBarVisible) {
|
||||
return _buttonBarHeight + _viewPaddingBottom;
|
||||
}
|
||||
return _viewPaddingBottom;
|
||||
}
|
||||
|
||||
void _positionWidget() {
|
||||
// Determine the center of the object being dragged so we can decide
|
||||
// in which corner the object should be placed.
|
||||
var dx = (_width / 2) + _left;
|
||||
dx = dx < 0 ? 0 : dx >= widget.availableScreenSize.width ? widget.availableScreenSize.width - 1 : dx;
|
||||
var dy = (_height / 2) + _top;
|
||||
dy = dy < 0 ? 0 : dy >= widget.availableScreenSize.height ? widget.availableScreenSize.height - 1 : dy;
|
||||
final draggableCenter = Offset(dx, dy);
|
||||
|
||||
setState(() {
|
||||
_duration = _duration300ms;
|
||||
if (Rect.fromLTRB(0, 0, widget.availableScreenSize.width / 2, widget.availableScreenSize.height / 2).contains(draggableCenter)) {
|
||||
// Top-left
|
||||
_top = _getCurrentStatusBarHeight() + _padding;
|
||||
_left = _padding;
|
||||
} else if (Rect.fromLTRB(widget.availableScreenSize.width / 2, 0, widget.availableScreenSize.width, widget.availableScreenSize.height / 2).contains(draggableCenter)) {
|
||||
// Top-right
|
||||
_top = _getCurrentStatusBarHeight() + _padding;
|
||||
_left = widget.availableScreenSize.width - _padding - _width;
|
||||
} else if (Rect.fromLTRB(0, widget.availableScreenSize.height / 2, widget.availableScreenSize.width / 2, widget.availableScreenSize.height).contains(draggableCenter)) {
|
||||
// Bottom-left
|
||||
_top = widget.availableScreenSize.height - (_getCurrentButtonBarHeight() + _padding) - _height;
|
||||
_left = _padding;
|
||||
} else if (Rect.fromLTRB(widget.availableScreenSize.width / 2, widget.availableScreenSize.height / 2, widget.availableScreenSize.width, widget.availableScreenSize.height).contains(draggableCenter)) {
|
||||
// Bottom-right
|
||||
_top = widget.availableScreenSize.height - (_getCurrentButtonBarHeight() + _padding) - _height;
|
||||
_left = widget.availableScreenSize.width - _padding - _width;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,106 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ButtonToProgress extends StatefulWidget {
|
||||
final double height;
|
||||
final double progressHeight;
|
||||
final String loadingText;
|
||||
final Duration duration;
|
||||
final TextStyle? loadingTextStyle;
|
||||
final VoidCallback? onPressed;
|
||||
final Stream<bool> onLoading;
|
||||
final Widget child;
|
||||
|
||||
const ButtonToProgress({
|
||||
Key? key,
|
||||
this.height = 40.0,
|
||||
this.progressHeight = 5.0,
|
||||
required this.loadingText,
|
||||
this.duration = const Duration(milliseconds: 300),
|
||||
this.loadingTextStyle,
|
||||
this.onPressed,
|
||||
required this.onLoading,
|
||||
required this.child,
|
||||
}) : assert(child != null),
|
||||
assert(height != null && height > 0),
|
||||
assert(progressHeight != null && progressHeight > 0 && progressHeight <= height),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
_ButtonToProgressState createState() => _ButtonToProgressState();
|
||||
}
|
||||
|
||||
class _ButtonToProgressState extends State<ButtonToProgress> {
|
||||
late double _height;
|
||||
double _opacity = 0;
|
||||
bool _isLoading = false;
|
||||
|
||||
late StreamSubscription<bool> _subscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_height = widget.height;
|
||||
if (widget.onLoading != null) {
|
||||
_subscription = widget.onLoading.listen((bool isLoading) {
|
||||
setState(() {
|
||||
_isLoading = isLoading;
|
||||
_height = isLoading ? widget.progressHeight : widget.height;
|
||||
_opacity = isLoading ? 1 : 0;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (_subscription != null) {
|
||||
_subscription.cancel();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: widget.height,
|
||||
child: Stack(
|
||||
children: [
|
||||
if (widget.loadingText == null)
|
||||
Container()
|
||||
else
|
||||
Padding(
|
||||
padding: EdgeInsets.only(bottom: widget.progressHeight),
|
||||
child: AnimatedOpacity(
|
||||
child: Center(
|
||||
child: FittedBox(
|
||||
child: Text(
|
||||
widget.loadingText,
|
||||
style: widget.loadingTextStyle,
|
||||
),
|
||||
),
|
||||
),
|
||||
opacity: _opacity,
|
||||
duration: Duration(milliseconds: widget.duration.inMilliseconds + 200),
|
||||
curve: Curves.easeInCubic,
|
||||
),
|
||||
),
|
||||
AnimatedPadding(
|
||||
duration: widget.duration,
|
||||
padding: EdgeInsets.only(
|
||||
top: math.max(widget.height - _height, 0),
|
||||
),
|
||||
child: AnimatedContainer(
|
||||
duration: widget.duration,
|
||||
height: _height,
|
||||
width: double.infinity,
|
||||
child: _isLoading ? const LinearProgressIndicator() : widget.child,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,103 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:native_device_orientation/native_device_orientation.dart';
|
||||
|
||||
class CircleButton extends StatefulWidget {
|
||||
final VoidCallback? onLongPress;
|
||||
final VoidCallback? onPressed;
|
||||
final GestureTapDownCallback? onTapDown;
|
||||
final VoidCallback? onTapCancel;
|
||||
final Widget child;
|
||||
final Color? color;
|
||||
final double radius;
|
||||
|
||||
const CircleButton({
|
||||
Key? key,
|
||||
this.onLongPress,
|
||||
this.onPressed,
|
||||
required this.child,
|
||||
this.color,
|
||||
this.radius = 25.0,
|
||||
this.onTapCancel,
|
||||
this.onTapDown,
|
||||
}) : assert(radius != null),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
_CircleButtonState createState() => _CircleButtonState();
|
||||
}
|
||||
|
||||
class _CircleButtonState extends State<CircleButton> {
|
||||
double _rotationAngle = 0.0;
|
||||
|
||||
final Stream<NativeDeviceOrientation> _orientationStream = NativeDeviceOrientationCommunicator().onOrientationChanged(useSensor: true);
|
||||
late StreamSubscription<NativeDeviceOrientation> _orientationSubscription;
|
||||
|
||||
void _handleOrientationChange(NativeDeviceOrientation orientation) {
|
||||
var targetAngle = 0.0;
|
||||
switch (orientation) {
|
||||
case NativeDeviceOrientation.unknown:
|
||||
case NativeDeviceOrientation.portraitUp:
|
||||
targetAngle = 0.0;
|
||||
break;
|
||||
case NativeDeviceOrientation.portraitDown:
|
||||
targetAngle = 180.0;
|
||||
break;
|
||||
case NativeDeviceOrientation.landscapeLeft:
|
||||
targetAngle = 90.0;
|
||||
break;
|
||||
case NativeDeviceOrientation.landscapeRight:
|
||||
targetAngle = 270.0;
|
||||
break;
|
||||
}
|
||||
setState(() {
|
||||
_rotationAngle = targetAngle;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_orientationSubscription = _orientationStream.listen(
|
||||
_handleOrientationChange,
|
||||
onError: (dynamic err) => print(err),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_orientationSubscription.cancel();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = 2 * widget.radius;
|
||||
|
||||
return Container(
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: BoxDecoration(
|
||||
color: (widget.color ?? Colors.blue).withAlpha(200),
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(widget.radius),
|
||||
),
|
||||
),
|
||||
child: GestureDetector(
|
||||
onLongPress: widget.onLongPress,
|
||||
onTapDown: widget.onTapDown,
|
||||
onTapCancel: widget.onTapCancel,
|
||||
child: RawMaterialButton(
|
||||
onPressed: widget.onPressed,
|
||||
child: RotationTransition(
|
||||
child: widget.child,
|
||||
turns: AlwaysStoppedAnimation<double>(_rotationAngle / 360),
|
||||
),
|
||||
elevation: 0,
|
||||
shape: const CircleBorder(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,147 @@
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum NoiseBoxDensity {
|
||||
high,
|
||||
medium,
|
||||
low,
|
||||
xHigh,
|
||||
xLow,
|
||||
}
|
||||
|
||||
class NoiseBox extends StatefulWidget {
|
||||
final NoiseBoxDensity density;
|
||||
final Color backgroundColor;
|
||||
final Widget? child;
|
||||
|
||||
const NoiseBox({
|
||||
Key? key,
|
||||
required this.backgroundColor,
|
||||
this.child,
|
||||
this.density = NoiseBoxDensity.medium,
|
||||
}) : assert(density != null),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
_NoiseBoxState createState() => _NoiseBoxState();
|
||||
}
|
||||
|
||||
class _NoiseBoxState extends State<NoiseBox> with TickerProviderStateMixin {
|
||||
late AnimationController _animationController;
|
||||
late int _density;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_animationController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(seconds: 60),
|
||||
);
|
||||
_animationController.repeat();
|
||||
switch (widget.density) {
|
||||
case NoiseBoxDensity.high:
|
||||
_density = 5;
|
||||
break;
|
||||
case NoiseBoxDensity.medium:
|
||||
_density = 7;
|
||||
break;
|
||||
case NoiseBoxDensity.low:
|
||||
_density = 10;
|
||||
break;
|
||||
case NoiseBoxDensity.xHigh:
|
||||
_density = 3;
|
||||
break;
|
||||
case NoiseBoxDensity.xLow:
|
||||
_density = 12;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) => Container(
|
||||
color: widget.backgroundColor,
|
||||
width: constraints.biggest.width,
|
||||
height: constraints.biggest.height,
|
||||
child: AnimatedBuilder(
|
||||
animation: _animationController,
|
||||
builder: (BuildContext context, Widget? child) {
|
||||
final children = <Widget>[
|
||||
CustomPaint(
|
||||
painter: NoisePainter(
|
||||
width: constraints.biggest.width,
|
||||
height: constraints.biggest.height,
|
||||
density: _density,
|
||||
),
|
||||
),
|
||||
];
|
||||
if (widget.child != null) {
|
||||
children.add(widget.child!);
|
||||
}
|
||||
return Stack(
|
||||
children: children,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class NoisePainter extends CustomPainter {
|
||||
final double width;
|
||||
final double height;
|
||||
final int density;
|
||||
|
||||
NoisePainter({
|
||||
required this.width,
|
||||
required this.height,
|
||||
required this.density,
|
||||
}) : assert(width != null),
|
||||
assert(height != null),
|
||||
assert(density != null &&
|
||||
density >= 3 &&
|
||||
density < math.min(width, height));
|
||||
|
||||
List<Color> colors = <Color>[
|
||||
Colors.black,
|
||||
Colors.grey,
|
||||
Colors.blueGrey,
|
||||
Colors.red,
|
||||
Colors.green,
|
||||
Colors.blue,
|
||||
Colors.white,
|
||||
];
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final random = math.Random();
|
||||
for (var w = 0; w < width; w += density) {
|
||||
for (var h = 0; h < height; h += density) {
|
||||
final offset = Offset(
|
||||
random.nextDouble() * width,
|
||||
random.nextDouble() * height,
|
||||
);
|
||||
final paint = Paint();
|
||||
paint.color = colors[random.nextInt(colors.length)];
|
||||
paint.strokeWidth = random.nextDouble() * 2;
|
||||
|
||||
canvas.drawPoints(PointMode.points, <Offset>[offset], paint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(CustomPainter oldDelegate) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,98 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import './platform_widget.dart';
|
||||
|
||||
class PlatformAlertDialog extends PlatformWidget {
|
||||
PlatformAlertDialog({required this.title, required this.content, required this.defaultActionText, this.cancelActionText})
|
||||
: assert(title != null),
|
||||
assert(content != null),
|
||||
assert(defaultActionText != null);
|
||||
|
||||
final String title;
|
||||
final String content;
|
||||
final String defaultActionText;
|
||||
final String? cancelActionText;
|
||||
|
||||
Future<bool?> show(BuildContext context) async {
|
||||
return Platform.isIOS
|
||||
? await showCupertinoDialog<bool>(
|
||||
context: context,
|
||||
builder: (BuildContext context) => this,
|
||||
)
|
||||
: await showDialog<bool>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (BuildContext context) => this,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildCupertinoWidget(BuildContext context) {
|
||||
return CupertinoAlertDialog(
|
||||
title: Text(title),
|
||||
content: Text(content),
|
||||
actions: _buildActions(context),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildMaterialWidget(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(title),
|
||||
content: Text(content),
|
||||
actions: _buildActions(context),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildActions(BuildContext context) {
|
||||
final actions = <Widget>[];
|
||||
if (cancelActionText != null) {
|
||||
actions.add(
|
||||
PlatformAlertDialogAction(
|
||||
child: Text(cancelActionText!),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(false);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
actions.add(
|
||||
PlatformAlertDialogAction(
|
||||
child: Text(defaultActionText),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(true);
|
||||
},
|
||||
),
|
||||
);
|
||||
return actions;
|
||||
}
|
||||
}
|
||||
|
||||
class PlatformAlertDialogAction extends PlatformWidget {
|
||||
PlatformAlertDialogAction({
|
||||
required this.child,
|
||||
required this.onPressed,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
@override
|
||||
Widget buildCupertinoWidget(BuildContext context) {
|
||||
return CupertinoDialogAction(
|
||||
child: child,
|
||||
onPressed: onPressed,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildMaterialWidget(BuildContext context) {
|
||||
return FlatButton(
|
||||
child: child,
|
||||
onPressed: onPressed,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import './platform_alert_dialog.dart';
|
||||
|
||||
class PlatformExceptionAlertDialog extends PlatformAlertDialog {
|
||||
PlatformExceptionAlertDialog({
|
||||
String title = 'An error occurred',
|
||||
required Exception exception,
|
||||
}) : super(
|
||||
title: title,
|
||||
content: exception is PlatformException ? _message(exception) : exception.toString(),
|
||||
defaultActionText: 'OK',
|
||||
);
|
||||
|
||||
static String _message(PlatformException exception) {
|
||||
return _errors[exception.code] ?? (exception.details != null ? (exception.details['message'] ?? exception.message) : exception.message);
|
||||
}
|
||||
|
||||
static final Map<String, String> _errors = <String, String>{
|
||||
'ERROR_CODE': 'Error description...',
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
abstract class PlatformWidget extends StatelessWidget {
|
||||
Widget buildCupertinoWidget(BuildContext context);
|
||||
|
||||
Widget buildMaterialWidget(BuildContext context);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (Platform.isIOS) {
|
||||
return buildCupertinoWidget(context);
|
||||
}
|
||||
return buildMaterialWidget(context);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
typedef ResponsiveBuilder = Widget Function(
|
||||
BuildContext context,
|
||||
Size size,
|
||||
);
|
||||
|
||||
class ResponsiveSafeArea extends StatelessWidget {
|
||||
const ResponsiveSafeArea({
|
||||
required ResponsiveBuilder builder,
|
||||
Key? key,
|
||||
}) : responsiveBuilder = builder,
|
||||
assert(builder != null),
|
||||
super(key: key);
|
||||
|
||||
final ResponsiveBuilder responsiveBuilder;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
return responsiveBuilder(
|
||||
context,
|
||||
constraints.biggest,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,212 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:doctor_app_flutter/screens/live_care/web-rtc/signaling.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||
import 'package:http/io_client.dart';
|
||||
import 'package:signalr_core/signalr_core.dart';
|
||||
|
||||
class SignalRUtil {
|
||||
late String sourceUser;
|
||||
late String destinationUser;
|
||||
setContributors({required String caller, required String receiver}){
|
||||
this.sourceUser = caller;
|
||||
this.destinationUser = receiver;
|
||||
}
|
||||
|
||||
Function(bool)? onConnected;
|
||||
String hubName;
|
||||
SignalRUtil({required this.hubName});
|
||||
|
||||
|
||||
late HubConnection connectionHub;
|
||||
|
||||
closeConnection() async{
|
||||
if(connectionHub != null) {
|
||||
connectionHub.off('OnIncomingCallAsync');
|
||||
connectionHub.off('OnCallDeclinedAsync');
|
||||
connectionHub.off('OnCallAcceptedAsync');
|
||||
connectionHub.off('nHangUpAsync');
|
||||
connectionHub.off('OnIceCandidateAsync');
|
||||
connectionHub.off('OnOfferAsync');
|
||||
await connectionHub.stop();
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> openConnection() async {
|
||||
connectionHub = HubConnectionBuilder()
|
||||
.withUrl(
|
||||
hubName,
|
||||
HttpConnectionOptions(
|
||||
logMessageContent: true,
|
||||
client: IOClient(HttpClient()..badCertificateCallback = (x, y, z) => true),
|
||||
logging: (level, message) => print(message),
|
||||
)).build();
|
||||
|
||||
await connectionHub.start();
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
|
||||
connectionHub.on('ReceiveMessage', (message) {
|
||||
handleIncomingMessage(message);
|
||||
});
|
||||
|
||||
return getConnectionState();
|
||||
}
|
||||
|
||||
void handleIncomingMessage(List<dynamic>? message) {
|
||||
print(message.toString());
|
||||
}
|
||||
|
||||
void sendMessage(List<dynamic> args) async {
|
||||
await connectionHub.invoke('SendMessage', args: args); //['Bob', 'Says hi!']
|
||||
}
|
||||
|
||||
listen({ required Function(CallUser) onAcceptCall, onHangupCall, required Function(Map, CallUser) onDeclineCall, onSdpOffer, onSdpAnswer, required Function(Map) onCandidate}){
|
||||
|
||||
connectionHub.on('OnIncomingCallAsync', (arguments) {
|
||||
print('OnIncomingCallAsync: ${arguments.toString()}');
|
||||
});
|
||||
|
||||
connectionHub.on('OnCallDeclinedAsync', (arguments) {
|
||||
print('OnCallDeclinedAsync: ${arguments.toString()}');
|
||||
final data = json.decode(arguments?.first);
|
||||
onDeclineCall(data, CallUser.from(arguments?.last));
|
||||
});
|
||||
|
||||
connectionHub.on('OnCallAcceptedAsync', (arguments) {
|
||||
print('OnCallAcceptedAsync: ${arguments.toString()}');
|
||||
onAcceptCall(CallUser.from(arguments?.last));
|
||||
});
|
||||
|
||||
connectionHub.on('OnHangUpAsync', (arguments) {
|
||||
print('nHangUpAsync: ${arguments.toString()}');
|
||||
onHangupCall(CallUser.from(arguments?.first));
|
||||
});
|
||||
|
||||
connectionHub.on('OnIceCandidateAsync', (arguments) {
|
||||
print('OnIceCandidateAsync: ${arguments.toString()}');
|
||||
final data = json.decode(arguments?.first);
|
||||
onCandidate(data);
|
||||
});
|
||||
|
||||
connectionHub.on('OnOfferAsync', (arguments) {
|
||||
print('OnOfferAsync: ${arguments.toString()}');
|
||||
final data = json.decode(arguments?.first);
|
||||
onSdpOffer(data, CallUser.from(arguments?.last));
|
||||
});
|
||||
|
||||
connectionHub.on('OnAnswerOffer', (arguments) {
|
||||
print('OnAnswerOffer: ${arguments.toString()}');
|
||||
final sdp = json.decode(arguments?.first)['sdp'];
|
||||
onSdpAnswer(sdp);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
// CallUserAsyncMobile(string currentUserId, string targerUserId, string patientInfoJson)
|
||||
Future<dynamic> callUserMobile(String from, to) async{
|
||||
final p_info = patientInfo(from, to, PATIENT_PUSH_TOKEN);
|
||||
return await connectionHub.invoke('CallUserAsyncMobile', args: [p_info]);
|
||||
}
|
||||
|
||||
// CallUserAsyncRemote(string currentUserId, string targerUserId)
|
||||
Future<dynamic> callUserRemote(String from, to) async{
|
||||
return await connectionHub.invoke('CallUserAsyncRemote', args: [from, to]);
|
||||
}
|
||||
|
||||
// CallUserAsync(string currentUserId, string targerUserId, string patientInfoJson)
|
||||
Future<dynamic> callUserWeb(String from, to) async{
|
||||
final p_info = patientInfo(from, to, PATIENT_PUSH_TOKEN);
|
||||
return await connectionHub.invoke('CallUserAsync', args: [from, to, p_info]);
|
||||
}
|
||||
patientInfo(String from, to, patientToken){
|
||||
final json_ = {
|
||||
"SessionID" : '123456',
|
||||
"Sess_token" : 'admin',
|
||||
"_Token" : patientToken,
|
||||
"AppointmentNo" : '123456',
|
||||
'ProjectID' : '12',
|
||||
"projectName" : 'Olaya',
|
||||
'doctorname' : 'Dr.Habib',
|
||||
'Docspeciality' : 'Cardiologist',
|
||||
'ClincName' : 'Cardiology',
|
||||
'PatientID' : to,
|
||||
'callerID' : from
|
||||
};
|
||||
return json.encode(json_);
|
||||
}
|
||||
|
||||
// CallDeclinedAsync(string currentUserId, string targerUserId)
|
||||
Future<dynamic> declineCall(String from, to) async{
|
||||
return await connectionHub.invoke('CallDeclinedAsync', args: [from, to]);
|
||||
}
|
||||
|
||||
// AnswerCallAsync(string currentUserId, string targetUserId)
|
||||
Future<dynamic> answerCall(String from, to) async{
|
||||
return await connectionHub.invoke('AnswerCallAsync', args: [from, to]);
|
||||
}
|
||||
|
||||
// IceCandidateAsync(string targetUserId, string candidate)
|
||||
Future<dynamic> addIceCandidate(RTCIceCandidate candidate) async{
|
||||
final target = destinationUser;
|
||||
final _candidate = json.encode({'candidate' : candidate.toMap()});
|
||||
return await connectionHub.invoke('IceCandidateAsync', args: [target, _candidate]);
|
||||
}
|
||||
|
||||
// OfferAsync(string targetUserId,string currentUserId, string targetOffer)
|
||||
Future<dynamic> offer(String from, to, offer) async{
|
||||
return await connectionHub.invoke('OfferAsync', args: [from, to, offer]);
|
||||
}
|
||||
|
||||
// AnswerOfferAsync(string targetUserId, string CallerOffer)
|
||||
Future<dynamic> answerOffer(RTCSessionDescription? answerSdp, caller, receiver) async{
|
||||
final payload = {
|
||||
'target': receiver,
|
||||
'caller': caller,
|
||||
'sdp': answerSdp?.toMap() ?? {},
|
||||
};
|
||||
return await connectionHub.invoke('AnswerOfferAsync', args: [caller, jsonEncode(payload)]);
|
||||
}
|
||||
|
||||
// HangUpAsync(string currentUserId, string targetUserId)
|
||||
Future<dynamic> hangupCall(String from, to) async{
|
||||
return await connectionHub.invoke('HangUpAsync', args: [from, to]);
|
||||
}
|
||||
|
||||
// CallAccepted(string currentUserId,string targetUserId)
|
||||
Future<dynamic> acceptCall(String from, to) async{
|
||||
// return await connectionHub.send(methodName: 'CallAccepted', args: [from, to]);
|
||||
return await connectionHub.invoke("CallAccepted", args: [ from, to]);
|
||||
}
|
||||
|
||||
|
||||
bool getConnectionState() {
|
||||
if (connectionHub.state == HubConnectionState.connected) return true;
|
||||
if (connectionHub.state == HubConnectionState.disconnected) return false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class CallUser{
|
||||
String? Id;
|
||||
String? UserName;
|
||||
String? Email;
|
||||
String? Phone;
|
||||
String? Title;
|
||||
dynamic? UserStatus;
|
||||
String? Image;
|
||||
int UnreadMessageCount = 0;
|
||||
|
||||
CallUser.from(Map map){
|
||||
Id = map['Id'];
|
||||
UserName = map['UserName'];
|
||||
Email = map['Email'];
|
||||
Phone = map['Phone'];
|
||||
Title = map['Title'];
|
||||
UserStatus = map['UserStatus'];
|
||||
Image = map['Image'];
|
||||
UnreadMessageCount = map['UnreadMessageCount'];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,98 @@
|
||||
v=0
|
||||
o=- 5927491327118611904 2 IN IP4 127.0.0.1
|
||||
s=-
|
||||
t=0 0
|
||||
a=group:BUNDLE audio video
|
||||
a=extmap-allow-mixed
|
||||
a=msid-semantic: WMS e53e47c9-d409-4522-bcc9-49c8b1d093b7
|
||||
m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 102 0 8 106 105 13 110 112 113 126
|
||||
c=IN IP4 0.0.0.0
|
||||
a=rtcp:9 IN IP4 0.0.0.0
|
||||
a=ice-ufrag:TPqz
|
||||
a=ice-pwd:Av/F5Hz76zQdybHAG2DomDog
|
||||
a=ice-options:trickle renomination
|
||||
a=fingerprint:sha-256 1F:D1:DD:59:42:65:91:E2:41:99:ED:6C:DB:30:76:4A:6B:5A:F9:82:7C:07:29:66:80:DC:AF:E2:34:05:43:18
|
||||
a=setup:actpass
|
||||
a=mid:audio
|
||||
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
|
||||
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
|
||||
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
|
||||
a=sendrecv
|
||||
a=rtcp-mux
|
||||
a=rtpmap:111 opus/48000/2
|
||||
a=rtcp-fb:111 transport-cc
|
||||
a=fmtp:111 minptime=10;useinbandfec=1
|
||||
a=rtpmap:103 ISAC/16000
|
||||
a=rtpmap:104 ISAC/32000
|
||||
a=rtpmap:9 G722/8000
|
||||
a=rtpmap:102 ILBC/8000
|
||||
a=rtpmap:0 PCMU/8000
|
||||
a=rtpmap:8 PCMA/8000
|
||||
a=rtpmap:106 CN/32000
|
||||
a=rtpmap:105 CN/16000
|
||||
a=rtpmap:13 CN/8000
|
||||
a=rtpmap:110 telephone-event/48000
|
||||
a=rtpmap:112 telephone-event/32000
|
||||
a=rtpmap:113 telephone-event/16000
|
||||
a=rtpmap:126 telephone-event/8000
|
||||
a=ssrc:3000425778 cname:kibCM8KmL1PJhPlQ
|
||||
a=ssrc:3000425778 msid:e53e47c9-d409-4522-bcc9-49c8b1d093b7 a82ca475-9a94-4bdc-a91c-1137d80a0753
|
||||
a=ssrc:3000425778 mslabel:e53e47c9-d409-4522-bcc9-49c8b1d093b7
|
||||
a=ssrc:3000425778 label:a82ca475-9a94-4bdc-a91c-1137d80a0753
|
||||
m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 35 36 100 101 127
|
||||
c=IN IP4 0.0.0.0
|
||||
a=rtcp:9 IN IP4 0.0.0.0
|
||||
a=ice-ufrag:TPqz
|
||||
a=ice-pwd:Av/F5Hz76zQdybHAG2DomDog
|
||||
a=ice-options:trickle renomination
|
||||
a=fingerprint:sha-256 1F:D1:DD:59:42:65:91:E2:41:99:ED:6C:DB:30:76:4A:6B:5A:F9:82:7C:07:29:66:80:DC:AF:E2:34:05:43:18
|
||||
a=setup:actpass
|
||||
a=mid:video
|
||||
a=extmap:14 urn:ietf:params:rtp-hdrext:toffset
|
||||
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
|
||||
a=extmap:13 urn:3gpp:video-orientation
|
||||
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
|
||||
a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
|
||||
a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type
|
||||
a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing
|
||||
a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space
|
||||
a=sendrecv
|
||||
a=rtcp-mux
|
||||
a=rtcp-rsize
|
||||
a=rtpmap:96 VP8/90000
|
||||
a=rtcp-fb:96 goog-remb
|
||||
a=rtcp-fb:96 transport-cc
|
||||
a=rtcp-fb:96 ccm fir
|
||||
a=rtcp-fb:96 nack
|
||||
a=rtcp-fb:96 nack pli
|
||||
a=rtpmap:97 rtx/90000
|
||||
a=fmtp:97 apt=96
|
||||
a=rtpmap:98 VP9/90000
|
||||
a=rtcp-fb:98 goog-remb
|
||||
a=rtcp-fb:98 transport-cc
|
||||
a=rtcp-fb:98 ccm fir
|
||||
a=rtcp-fb:98 nack
|
||||
a=rtcp-fb:98 nack pli
|
||||
a=rtpmap:99 rtx/90000
|
||||
a=fmtp:99 apt=98
|
||||
a=rtpmap:35 AV1X/90000
|
||||
a=rtcp-fb:35 goog-remb
|
||||
a=rtcp-fb:35 transport-cc
|
||||
a=rtcp-fb:35 ccm fir
|
||||
a=rtcp-fb:35 nack
|
||||
a=rtcp-fb:35 nack pli
|
||||
a=rtpmap:36 rtx/90000
|
||||
a=fmtp:36 apt=35
|
||||
a=rtpmap:100 red/90000
|
||||
a=rtpmap:101 rtx/90000
|
||||
a=fmtp:101 apt=100
|
||||
a=rtpmap:127 ulpfec/90000
|
||||
a=ssrc-group:FID 1697527935 3482614104
|
||||
a=ssrc:1697527935 cname:kibCM8KmL1PJhPlQ
|
||||
a=ssrc:1697527935 msid:e53e47c9-d409-4522-bcc9-49c8b1d093b7 49e96524-ed6c-4fa3-9bc3-745d716698c0
|
||||
a=ssrc:1697527935 mslabel:e53e47c9-d409-4522-bcc9-49c8b1d093b7
|
||||
a=ssrc:1697527935 label:49e96524-ed6c-4fa3-9bc3-745d716698c0
|
||||
a=ssrc:3482614104 cname:kibCM8KmL1PJhPlQ
|
||||
a=ssrc:3482614104 msid:e53e47c9-d409-4522-bcc9-49c8b1d093b7 49e96524-ed6c-4fa3-9bc3-745d716698c0
|
||||
a=ssrc:3482614104 mslabel:e53e47c9-d409-4522-bcc9-49c8b1d093b7
|
||||
a=ssrc:3482614104 label:49e96524-ed6c-4fa3-9bc3-745d716698c0
|
||||
@ -0,0 +1,98 @@
|
||||
v=0
|
||||
o=- 1852792980555145345 2 IN IP4 127.0.0.1
|
||||
s=-
|
||||
t=0 0
|
||||
a=group:BUNDLE audio video
|
||||
a=extmap-allow-mixed
|
||||
a=msid-semantic: WMS c435b864-09cb-4e9b-b467-0215765153b2
|
||||
m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 102 0 8 106 105 13 110 112 113 126
|
||||
c=IN IP4 0.0.0.0
|
||||
a=rtcp:9 IN IP4 0.0.0.0
|
||||
a=ice-ufrag:yZwB
|
||||
a=ice-pwd:xoT7qMaStLtOryw8gBoh9HBE
|
||||
a=ice-options:trickle
|
||||
a=fingerprint:sha-256 E6:93:81:34:89:3A:AE:E9:66:B3:B1:BF:81:F4:F9:EF:71:84:41:21:09:98:BC:D2:0C:7F:98:EF:CA:E5:47:3A
|
||||
a=setup:actpass
|
||||
a=mid:audio
|
||||
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
|
||||
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
|
||||
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
|
||||
a=sendrecv
|
||||
a=rtcp-mux
|
||||
a=rtpmap:111 opus/48000/2
|
||||
a=rtcp-fb:111 transport-cc
|
||||
a=fmtp:111 minptime=10;useinbandfec=1
|
||||
a=rtpmap:103 ISAC/16000
|
||||
a=rtpmap:104 ISAC/32000
|
||||
a=rtpmap:9 G722/8000
|
||||
a=rtpmap:102 ILBC/8000
|
||||
a=rtpmap:0 PCMU/8000
|
||||
a=rtpmap:8 PCMA/8000
|
||||
a=rtpmap:106 CN/32000
|
||||
a=rtpmap:105 CN/16000
|
||||
a=rtpmap:13 CN/8000
|
||||
a=rtpmap:110 telephone-event/48000
|
||||
a=rtpmap:112 telephone-event/32000
|
||||
a=rtpmap:113 telephone-event/16000
|
||||
a=rtpmap:126 telephone-event/8000
|
||||
a=ssrc:467703518 cname:LpqUpo/SZG/oP5nR
|
||||
a=ssrc:467703518 msid:c435b864-09cb-4e9b-b467-0215765153b2 112bdcb4-6696-4473-921b-54a9b864134e
|
||||
a=ssrc:467703518 mslabel:c435b864-09cb-4e9b-b467-0215765153b2
|
||||
a=ssrc:467703518 label:112bdcb4-6696-4473-921b-54a9b864134e
|
||||
m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 35 36 100 101 127
|
||||
c=IN IP4 0.0.0.0
|
||||
a=rtcp:9 IN IP4 0.0.0.0
|
||||
a=ice-ufrag:mrRm
|
||||
a=ice-pwd:eQ/xWrHgvgZol4cryWYtf6kM
|
||||
a=ice-options:trickle renomination
|
||||
a=fingerprint:sha-256 08:BF:EB:45:0B:49:A0:51:B1:D8:45:4E:BA:03:81:98:95:64:0A:2A:27:0E:B6:2D:2B:E7:9E:C4:0E:A5:53:14
|
||||
a=setup:actpass
|
||||
a=mid:video
|
||||
a=extmap:14 urn:ietf:params:rtp-hdrext:toffset
|
||||
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
|
||||
a=extmap:13 urn:3gpp:video-orientation
|
||||
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
|
||||
a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
|
||||
a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type
|
||||
a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing
|
||||
a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space
|
||||
a=sendrecv
|
||||
a=rtcp-mux
|
||||
a=rtcp-rsize
|
||||
a=rtpmap:96 VP8/90000
|
||||
a=rtcp-fb:96 goog-remb
|
||||
a=rtcp-fb:96 transport-cc
|
||||
a=rtcp-fb:96 ccm fir
|
||||
a=rtcp-fb:96 nack
|
||||
a=rtcp-fb:96 nack pli
|
||||
a=rtpmap:97 rtx/90000
|
||||
a=fmtp:97 apt=96
|
||||
a=rtpmap:98 VP9/90000
|
||||
a=rtcp-fb:98 goog-remb
|
||||
a=rtcp-fb:98 transport-cc
|
||||
a=rtcp-fb:98 ccm fir
|
||||
a=rtcp-fb:98 nack
|
||||
a=rtcp-fb:98 nack pli
|
||||
a=rtpmap:99 rtx/90000
|
||||
a=fmtp:99 apt=98
|
||||
a=rtpmap:35 AV1X/90000
|
||||
a=rtcp-fb:35 goog-remb
|
||||
a=rtcp-fb:35 transport-cc
|
||||
a=rtcp-fb:35 ccm fir
|
||||
a=rtcp-fb:35 nack
|
||||
a=rtcp-fb:35 nack pli
|
||||
a=rtpmap:36 rtx/90000
|
||||
a=fmtp:36 apt=35
|
||||
a=rtpmap:100 red/90000
|
||||
a=rtpmap:101 rtx/90000
|
||||
a=fmtp:101 apt=100
|
||||
a=rtpmap:127 ulpfec/90000
|
||||
a=ssrc-group:FID 2760462647 1007249032
|
||||
a=ssrc:2760462647 cname:LpqUpo/SZG/oP5nR
|
||||
a=ssrc:2760462647 msid:c435b864-09cb-4e9b-b467-0215765153b2 8d8b378e-fc04-428d-a4df-50af401e70d2
|
||||
a=ssrc:2760462647 mslabel:c435b864-09cb-4e9b-b467-0215765153b2
|
||||
a=ssrc:2760462647 label:8d8b378e-fc04-428d-a4df-50af401e70d2
|
||||
a=ssrc:1007249032 cname:LpqUpo/SZG/oP5nR
|
||||
a=ssrc:1007249032 msid:c435b864-09cb-4e9b-b467-0215765153b2 8d8b378e-fc04-428d-a4df-50af401e70d2
|
||||
a=ssrc:1007249032 mslabel:c435b864-09cb-4e9b-b467-0215765153b2
|
||||
a=ssrc:1007249032 label:8d8b378e-fc04-428d-a4df-50af401e70d2
|
||||
@ -0,0 +1,109 @@
|
||||
v=0
|
||||
o=- 3297723553455155496 2 IN IP4 127.0.0.1
|
||||
s=-
|
||||
t=0 0
|
||||
a=group:BUNDLE 0 1
|
||||
a=extmap-allow-mixed
|
||||
a=msid-semantic: WMS h3MZrNWyvufOFj9hhh4M1dQjm53cZXMbEKxM
|
||||
m=audio 9 UDP/TLS/RTP/SAVPF 111 63 103 104 9 0 8 106 105 13 110 112 113 126
|
||||
c=IN IP4 0.0.0.0
|
||||
a=rtcp:9 IN IP4 0.0.0.0
|
||||
a=ice-ufrag:yZwB
|
||||
a=ice-pwd:xoT7qMaStLtOryw8gBoh9HBE
|
||||
a=ice-options:trickle
|
||||
a=fingerprint:sha-256 E6:93:81:34:89:3A:AE:E9:66:B3:B1:BF:81:F4:F9:EF:71:84:41:21:09:98:BC:D2:0C:7F:98:EF:CA:E5:47:3A
|
||||
a=setup:actpass
|
||||
a=mid:0
|
||||
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
|
||||
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
|
||||
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
|
||||
a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid
|
||||
a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
|
||||
a=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id
|
||||
a=sendrecv
|
||||
a=msid:h3MZrNWyvufOFj9hhh4M1dQjm53cZXMbEKxM ee14b0b6-bef2-4a88-8550-a7120c16eb41
|
||||
a=rtcp-mux
|
||||
a=rtpmap:111 opus/48000/2
|
||||
a=rtcp-fb:111 transport-cc
|
||||
a=fmtp:111 minptime=10;useinbandfec=1
|
||||
a=rtpmap:63 red/48000/2
|
||||
a=fmtp:63 111/111
|
||||
a=rtpmap:103 ISAC/16000
|
||||
a=rtpmap:104 ISAC/32000
|
||||
a=rtpmap:9 G722/8000
|
||||
a=rtpmap:0 PCMU/8000
|
||||
a=rtpmap:8 PCMA/8000
|
||||
a=rtpmap:106 CN/32000
|
||||
a=rtpmap:105 CN/16000
|
||||
a=rtpmap:13 CN/8000
|
||||
a=rtpmap:110 telephone-event/48000
|
||||
a=rtpmap:112 telephone-event/32000
|
||||
a=rtpmap:113 telephone-event/16000
|
||||
a=rtpmap:126 telephone-event/8000
|
||||
a=ssrc:2100092274 cname:dMYQoj76poQISooy
|
||||
a=ssrc:2100092274 msid:h3MZrNWyvufOFj9hhh4M1dQjm53cZXMbEKxM ee14b0b6-bef2-4a88-8550-a7120c16eb41
|
||||
a=ssrc:2100092274 mslabel:h3MZrNWyvufOFj9hhh4M1dQjm53cZXMbEKxM
|
||||
a=ssrc:2100092274 label:ee14b0b6-bef2-4a88-8550-a7120c16eb41
|
||||
m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 122 121 127
|
||||
c=IN IP4 0.0.0.0
|
||||
a=rtcp:9 IN IP4 0.0.0.0
|
||||
a=ice-ufrag:yZwB
|
||||
a=ice-pwd:xoT7qMaStLtOryw8gBoh9HBE
|
||||
a=ice-options:trickle
|
||||
a=fingerprint:sha-256 E6:93:81:34:89:3A:AE:E9:66:B3:B1:BF:81:F4:F9:EF:71:84:41:21:09:98:BC:D2:0C:7F:98:EF:CA:E5:47:3A
|
||||
a=setup:actpass
|
||||
a=mid:1
|
||||
a=extmap:14 urn:ietf:params:rtp-hdrext:toffset
|
||||
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
|
||||
a=extmap:13 urn:3gpp:video-orientation
|
||||
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
|
||||
a=extmap:12 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
|
||||
a=extmap:11 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type
|
||||
a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing
|
||||
a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space
|
||||
a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid
|
||||
a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
|
||||
a=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id
|
||||
a=sendrecv
|
||||
a=msid:h3MZrNWyvufOFj9hhh4M1dQjm53cZXMbEKxM 5c21bd40-52fb-4c67-9bd2-cd5b64dc9de1
|
||||
a=rtcp-mux
|
||||
a=rtcp-rsize
|
||||
a=rtpmap:96 VP8/90000
|
||||
a=rtcp-fb:96 goog-remb
|
||||
a=rtcp-fb:96 transport-cc
|
||||
a=rtcp-fb:96 ccm fir
|
||||
a=rtcp-fb:96 nack
|
||||
a=rtcp-fb:96 nack pli
|
||||
a=rtpmap:97 rtx/90000
|
||||
a=fmtp:97 apt=96
|
||||
a=rtpmap:98 VP9/90000
|
||||
a=rtcp-fb:98 goog-remb
|
||||
a=rtcp-fb:98 transport-cc
|
||||
a=rtcp-fb:98 ccm fir
|
||||
a=rtcp-fb:98 nack
|
||||
a=rtcp-fb:98 nack pli
|
||||
a=fmtp:98 profile-id=0
|
||||
a=rtpmap:99 rtx/90000
|
||||
a=fmtp:99 apt=98
|
||||
a=rtpmap:100 H264/90000
|
||||
a=rtcp-fb:100 goog-remb
|
||||
a=rtcp-fb:100 transport-cc
|
||||
a=rtcp-fb:100 ccm fir
|
||||
a=rtcp-fb:100 nack
|
||||
a=rtcp-fb:100 nack pli
|
||||
a=fmtp:100 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
|
||||
a=rtpmap:101 rtx/90000
|
||||
a=fmtp:101 apt=100
|
||||
a=rtpmap:122 red/90000
|
||||
a=rtpmap:121 rtx/90000
|
||||
a=fmtp:121 apt=122
|
||||
a=rtpmap:127 ulpfec/90000
|
||||
a=ssrc-group:FID 2717479063 1967595658
|
||||
a=ssrc:2717479063 cname:dMYQoj76poQISooy
|
||||
a=ssrc:2717479063 msid:h3MZrNWyvufOFj9hhh4M1dQjm53cZXMbEKxM 5c21bd40-52fb-4c67-9bd2-cd5b64dc9de1
|
||||
a=ssrc:2717479063 mslabel:h3MZrNWyvufOFj9hhh4M1dQjm53cZXMbEKxM
|
||||
a=ssrc:2717479063 label:5c21bd40-52fb-4c67-9bd2-cd5b64dc9de1
|
||||
a=ssrc:1967595658 cname:dMYQoj76poQISooy
|
||||
a=ssrc:1967595658 msid:h3MZrNWyvufOFj9hhh4M1dQjm53cZXMbEKxM 5c21bd40-52fb-4c67-9bd2-cd5b64dc9de1
|
||||
a=ssrc:1967595658 mslabel:h3MZrNWyvufOFj9hhh4M1dQjm53cZXMbEKxM
|
||||
a=ssrc:1967595658 label:5c21bd40-52fb-4c67-9bd2-cd5b64dc9de1
|
||||
@ -0,0 +1,114 @@
|
||||
v=0
|
||||
o=- 5927491327118611904 3 IN IP4 127.0.0.1
|
||||
s=-
|
||||
t=0 0
|
||||
a=group:BUNDLE audio video
|
||||
a=extmap-allow-mixed
|
||||
a=msid-semantic: WMS e53e47c9-d409-4522-bcc9-49c8b1d093b7
|
||||
m=audio 49168 UDP/TLS/RTP/SAVPF 111 103 104 9 102 0 8 106 105 13 110 112 113 126
|
||||
c=IN IP4 15.185.116.59
|
||||
a=rtcp:9 IN IP4 0.0.0.0
|
||||
a=candidate:2473295229 1 udp 2122262783 2a02:cb80:4078:40c3:18c0:72ff:fe47:4312 51566 typ host generation 0 network-id 4 network-cost 10
|
||||
a=candidate:3370617219 1 udp 2122194687 172.20.10.11 59256 typ host generation 0 network-id 3 network-cost 10
|
||||
a=candidate:559267639 1 udp 2122136831 ::1 57659 typ host generation 0 network-id 2
|
||||
a=candidate:1510613869 1 udp 2122063615 127.0.0.1 50875 typ host generation 0 network-id 1
|
||||
a=candidate:1876313031 1 tcp 1518157055 ::1 39063 typ host tcptype passive generation 0 network-id 2
|
||||
a=candidate:344579997 1 tcp 1518083839 127.0.0.1 49823 typ host tcptype passive generation 0 network-id 1
|
||||
a=candidate:842163049 1 udp 1685987071 51.36.24.254 1652 typ srflx raddr 172.20.10.11 rport 59256 generation 0 network-id 3 network-cost 10
|
||||
a=candidate:3951447094 1 udp 41819903 15.185.116.59 49168 typ relay raddr 51.36.24.254 rport 1652 generation 0 network-id 3 network-cost 10
|
||||
a=ice-ufrag:TPqz
|
||||
a=ice-pwd:Av/F5Hz76zQdybHAG2DomDog
|
||||
a=ice-options:trickle renomination
|
||||
a=fingerprint:sha-256 1F:D1:DD:59:42:65:91:E2:41:99:ED:6C:DB:30:76:4A:6B:5A:F9:82:7C:07:29:66:80:DC:AF:E2:34:05:43:18
|
||||
a=setup:actpass
|
||||
a=mid:audio
|
||||
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
|
||||
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
|
||||
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
|
||||
a=sendrecv
|
||||
a=rtcp-mux
|
||||
a=rtpmap:111 opus/48000/2
|
||||
a=rtcp-fb:111 transport-cc
|
||||
a=fmtp:111 minptime=10;useinbandfec=1
|
||||
a=rtpmap:103 ISAC/16000
|
||||
a=rtpmap:104 ISAC/32000
|
||||
a=rtpmap:9 G722/8000
|
||||
a=rtpmap:102 ILBC/8000
|
||||
a=rtpmap:0 PCMU/8000
|
||||
a=rtpmap:8 PCMA/8000
|
||||
a=rtpmap:106 CN/32000
|
||||
a=rtpmap:105 CN/16000
|
||||
a=rtpmap:13 CN/8000
|
||||
a=rtpmap:110 telephone-event/48000
|
||||
a=rtpmap:112 telephone-event/32000
|
||||
a=rtpmap:113 telephone-event/16000
|
||||
a=rtpmap:126 telephone-event/8000
|
||||
a=ssrc:3000425778 cname:kibCM8KmL1PJhPlQ
|
||||
a=ssrc:3000425778 msid:e53e47c9-d409-4522-bcc9-49c8b1d093b7 a82ca475-9a94-4bdc-a91c-1137d80a0753
|
||||
a=ssrc:3000425778 mslabel:e53e47c9-d409-4522-bcc9-49c8b1d093b7
|
||||
a=ssrc:3000425778 label:a82ca475-9a94-4bdc-a91c-1137d80a0753
|
||||
m=video 49057 UDP/TLS/RTP/SAVPF 96 97 98 99 35 36 100 101 127
|
||||
c=IN IP4 15.185.116.59
|
||||
a=rtcp:9 IN IP4 0.0.0.0
|
||||
a=candidate:2473295229 1 udp 2122262783 2a02:cb80:4078:40c3:18c0:72ff:fe47:4312 39324 typ host generation 0 network-id 4 network-cost 10
|
||||
a=candidate:3370617219 1 udp 2122194687 172.20.10.11 41527 typ host generation 0 network-id 3 network-cost 10
|
||||
a=candidate:559267639 1 udp 2122136831 ::1 37365 typ host generation 0 network-id 2
|
||||
a=candidate:1510613869 1 udp 2122063615 127.0.0.1 43321 typ host generation 0 network-id 1
|
||||
a=candidate:1876313031 1 tcp 1518157055 ::1 40159 typ host tcptype passive generation 0 network-id 2
|
||||
a=candidate:344579997 1 tcp 1518083839 127.0.0.1 36533 typ host tcptype passive generation 0 network-id 1
|
||||
a=candidate:842163049 1 udp 1685987071 51.36.24.254 1653 typ srflx raddr 172.20.10.11 rport 41527 generation 0 network-id 3 network-cost 10
|
||||
a=candidate:3951447094 1 udp 41819903 15.185.116.59 49057 typ relay raddr 51.36.24.254 rport 1653 generation 0 network-id 3 network-cost 10
|
||||
a=ice-ufrag:TPqz
|
||||
a=ice-pwd:Av/F5Hz76zQdybHAG2DomDog
|
||||
a=ice-options:trickle renomination
|
||||
a=fingerprint:sha-256 1F:D1:DD:59:42:65:91:E2:41:99:ED:6C:DB:30:76:4A:6B:5A:F9:82:7C:07:29:66:80:DC:AF:E2:34:05:43:18
|
||||
a=setup:actpass
|
||||
a=mid:video
|
||||
a=extmap:14 urn:ietf:params:rtp-hdrext:toffset
|
||||
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
|
||||
a=extmap:13 urn:3gpp:video-orientation
|
||||
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
|
||||
a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
|
||||
a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type
|
||||
a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing
|
||||
a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space
|
||||
a=sendrecv
|
||||
a=rtcp-mux
|
||||
a=rtcp-rsize
|
||||
a=rtpmap:96 VP8/90000
|
||||
a=rtcp-fb:96 goog-remb
|
||||
a=rtcp-fb:96 transport-cc
|
||||
a=rtcp-fb:96 ccm fir
|
||||
a=rtcp-fb:96 nack
|
||||
a=rtcp-fb:96 nack pli
|
||||
a=rtpmap:97 rtx/90000
|
||||
a=fmtp:97 apt=96
|
||||
a=rtpmap:98 VP9/90000
|
||||
a=rtcp-fb:98 goog-remb
|
||||
a=rtcp-fb:98 transport-cc
|
||||
a=rtcp-fb:98 ccm fir
|
||||
a=rtcp-fb:98 nack
|
||||
a=rtcp-fb:98 nack pli
|
||||
a=rtpmap:99 rtx/90000
|
||||
a=fmtp:99 apt=98
|
||||
a=rtpmap:35 AV1X/90000
|
||||
a=rtcp-fb:35 goog-remb
|
||||
a=rtcp-fb:35 transport-cc
|
||||
a=rtcp-fb:35 ccm fir
|
||||
a=rtcp-fb:35 nack
|
||||
a=rtcp-fb:35 nack pli
|
||||
a=rtpmap:36 rtx/90000
|
||||
a=fmtp:36 apt=35
|
||||
a=rtpmap:100 red/90000
|
||||
a=rtpmap:101 rtx/90000
|
||||
a=fmtp:101 apt=100
|
||||
a=rtpmap:127 ulpfec/90000
|
||||
a=ssrc-group:FID 1697527935 3482614104
|
||||
a=ssrc:1697527935 cname:kibCM8KmL1PJhPlQ
|
||||
a=ssrc:1697527935 msid:e53e47c9-d409-4522-bcc9-49c8b1d093b7 49e96524-ed6c-4fa3-9bc3-745d716698c0
|
||||
a=ssrc:1697527935 mslabel:e53e47c9-d409-4522-bcc9-49c8b1d093b7
|
||||
a=ssrc:1697527935 label:49e96524-ed6c-4fa3-9bc3-745d716698c0
|
||||
a=ssrc:3482614104 cname:kibCM8KmL1PJhPlQ
|
||||
a=ssrc:3482614104 msid:e53e47c9-d409-4522-bcc9-49c8b1d093b7 49e96524-ed6c-4fa3-9bc3-745d716698c0
|
||||
a=ssrc:3482614104 mslabel:e53e47c9-d409-4522-bcc9-49c8b1d093b7
|
||||
a=ssrc:3482614104 label:49e96524-ed6c-4fa3-9bc3-745d716698c0
|
||||
@ -0,0 +1,110 @@
|
||||
v=0
|
||||
o=- 1852792980555145345 3 IN IP4 127.0.0.1
|
||||
s=-
|
||||
t=0 0
|
||||
a=group:BUNDLE audio video
|
||||
a=extmap-allow-mixed
|
||||
a=msid-semantic: WMS c435b864-09cb-4e9b-b467-0215765153b2
|
||||
m=audio 2488 UDP/TLS/RTP/SAVPF 111 103 104 9 102 0 8 106 105 13 110 112 113 126
|
||||
c=IN IP4 51.39.198.194
|
||||
a=rtcp:9 IN IP4 0.0.0.0
|
||||
a=candidate:2080483951 1 udp 2122260223 192.168.8.146 53264 typ host generation 0 network-id 3 network-cost 10
|
||||
a=candidate:559267639 1 udp 2122202367 ::1 58708 typ host generation 0 network-id 2
|
||||
a=candidate:1510613869 1 udp 2122129151 127.0.0.1 41026 typ host generation 0 network-id 1
|
||||
a=candidate:1876313031 1 tcp 1518222591 ::1 40989 typ host tcptype passive generation 0 network-id 2
|
||||
a=candidate:344579997 1 tcp 1518149375 127.0.0.1 58889 typ host tcptype passive generation 0 network-id 1
|
||||
a=candidate:842163049 1 udp 1686052607 51.39.198.194 2488 typ srflx raddr 192.168.8.146 rport 53264 generation 0 network-id 3 network-cost 10
|
||||
a=ice-ufrag:mrRm
|
||||
a=ice-pwd:eQ/xWrHgvgZol4cryWYtf6kM
|
||||
a=ice-options:trickle renomination
|
||||
a=fingerprint:sha-256 08:BF:EB:45:0B:49:A0:51:B1:D8:45:4E:BA:03:81:98:95:64:0A:2A:27:0E:B6:2D:2B:E7:9E:C4:0E:A5:53:14
|
||||
a=setup:actpass
|
||||
a=mid:audio
|
||||
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
|
||||
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
|
||||
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
|
||||
a=sendrecv
|
||||
a=rtcp-mux
|
||||
a=rtpmap:111 opus/48000/2
|
||||
a=rtcp-fb:111 transport-cc
|
||||
a=fmtp:111 minptime=10;useinbandfec=1
|
||||
a=rtpmap:103 ISAC/16000
|
||||
a=rtpmap:104 ISAC/32000
|
||||
a=rtpmap:9 G722/8000
|
||||
a=rtpmap:102 ILBC/8000
|
||||
a=rtpmap:0 PCMU/8000
|
||||
a=rtpmap:8 PCMA/8000
|
||||
a=rtpmap:106 CN/32000
|
||||
a=rtpmap:105 CN/16000
|
||||
a=rtpmap:13 CN/8000
|
||||
a=rtpmap:110 telephone-event/48000
|
||||
a=rtpmap:112 telephone-event/32000
|
||||
a=rtpmap:113 telephone-event/16000
|
||||
a=rtpmap:126 telephone-event/8000
|
||||
a=ssrc:467703518 cname:LpqUpo/SZG/oP5nR
|
||||
a=ssrc:467703518 msid:c435b864-09cb-4e9b-b467-0215765153b2 112bdcb4-6696-4473-921b-54a9b864134e
|
||||
a=ssrc:467703518 mslabel:c435b864-09cb-4e9b-b467-0215765153b2
|
||||
a=ssrc:467703518 label:112bdcb4-6696-4473-921b-54a9b864134e
|
||||
m=video 2485 UDP/TLS/RTP/SAVPF 96 97 98 99 35 36 100 101 127
|
||||
c=IN IP4 51.39.198.194
|
||||
a=rtcp:9 IN IP4 0.0.0.0
|
||||
a=candidate:2080483951 1 udp 2122260223 192.168.8.146 33465 typ host generation 0 network-id 3 network-cost 10
|
||||
a=candidate:559267639 1 udp 2122202367 ::1 33641 typ host generation 0 network-id 2
|
||||
a=candidate:1510613869 1 udp 2122129151 127.0.0.1 41957 typ host generation 0 network-id 1
|
||||
a=candidate:1876313031 1 tcp 1518222591 ::1 46553 typ host tcptype passive generation 0 network-id 2
|
||||
a=candidate:344579997 1 tcp 1518149375 127.0.0.1 41591 typ host tcptype passive generation 0 network-id 1
|
||||
a=candidate:842163049 1 udp 1686052607 51.39.198.194 2485 typ srflx raddr 192.168.8.146 rport 33465 generation 0 network-id 3 network-cost 10
|
||||
a=ice-ufrag:mrRm
|
||||
a=ice-pwd:eQ/xWrHgvgZol4cryWYtf6kM
|
||||
a=ice-options:trickle renomination
|
||||
a=fingerprint:sha-256 08:BF:EB:45:0B:49:A0:51:B1:D8:45:4E:BA:03:81:98:95:64:0A:2A:27:0E:B6:2D:2B:E7:9E:C4:0E:A5:53:14
|
||||
a=setup:actpass
|
||||
a=mid:video
|
||||
a=extmap:14 urn:ietf:params:rtp-hdrext:toffset
|
||||
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
|
||||
a=extmap:13 urn:3gpp:video-orientation
|
||||
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
|
||||
a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
|
||||
a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type
|
||||
a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing
|
||||
a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space
|
||||
a=sendrecv
|
||||
a=rtcp-mux
|
||||
a=rtcp-rsize
|
||||
a=rtpmap:96 VP8/90000
|
||||
a=rtcp-fb:96 goog-remb
|
||||
a=rtcp-fb:96 transport-cc
|
||||
a=rtcp-fb:96 ccm fir
|
||||
a=rtcp-fb:96 nack
|
||||
a=rtcp-fb:96 nack pli
|
||||
a=rtpmap:97 rtx/90000
|
||||
a=fmtp:97 apt=96
|
||||
a=rtpmap:98 VP9/90000
|
||||
a=rtcp-fb:98 goog-remb
|
||||
a=rtcp-fb:98 transport-cc
|
||||
a=rtcp-fb:98 ccm fir
|
||||
a=rtcp-fb:98 nack
|
||||
a=rtcp-fb:98 nack pli
|
||||
a=rtpmap:99 rtx/90000
|
||||
a=fmtp:99 apt=98
|
||||
a=rtpmap:35 AV1X/90000
|
||||
a=rtcp-fb:35 goog-remb
|
||||
a=rtcp-fb:35 transport-cc
|
||||
a=rtcp-fb:35 ccm fir
|
||||
a=rtcp-fb:35 nack
|
||||
a=rtcp-fb:35 nack pli
|
||||
a=rtpmap:36 rtx/90000
|
||||
a=fmtp:36 apt=35
|
||||
a=rtpmap:100 red/90000
|
||||
a=rtpmap:101 rtx/90000
|
||||
a=fmtp:101 apt=100
|
||||
a=rtpmap:127 ulpfec/90000
|
||||
a=ssrc-group:FID 2760462647 1007249032
|
||||
a=ssrc:2760462647 cname:LpqUpo/SZG/oP5nR
|
||||
a=ssrc:2760462647 msid:c435b864-09cb-4e9b-b467-0215765153b2 8d8b378e-fc04-428d-a4df-50af401e70d2
|
||||
a=ssrc:2760462647 mslabel:c435b864-09cb-4e9b-b467-0215765153b2
|
||||
a=ssrc:2760462647 label:8d8b378e-fc04-428d-a4df-50af401e70d2
|
||||
a=ssrc:1007249032 cname:LpqUpo/SZG/oP5nR
|
||||
a=ssrc:1007249032 msid:c435b864-09cb-4e9b-b467-0215765153b2 8d8b378e-fc04-428d-a4df-50af401e70d2
|
||||
a=ssrc:1007249032 mslabel:c435b864-09cb-4e9b-b467-0215765153b2
|
||||
a=ssrc:1007249032 label:8d8b378e-fc04-428d-a4df-50af401e70d2
|
||||
Loading…
Reference in New Issue