Web RTC Design implementation
parent
999a45c711
commit
ef2bcc1c65
@ -0,0 +1,172 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:diplomaticquarterapp/pages/conference/web_rtc/widgets/cam_view_widget.dart';
|
||||
import 'package:diplomaticquarterapp/pages/conference/widgets/noise_box.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 {
|
||||
@override
|
||||
_CallHomePageState createState() => _CallHomePageState();
|
||||
}
|
||||
|
||||
class _CallHomePageState extends State<CallHomePage> {
|
||||
bool showNoise = false;
|
||||
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
|
||||
MediaStream stream;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
// TODO: implement initState
|
||||
super.initState();
|
||||
_localRenderer.initialize();
|
||||
_remoteRenderer.initialize();
|
||||
|
||||
enableVideo();
|
||||
}
|
||||
|
||||
enableVideo() async {
|
||||
//Stream to enable video
|
||||
stream = await navigator.mediaDevices.getUserMedia({'video': true, 'audio': true});
|
||||
// _audioButton.add(false);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// TODO: implement dispose
|
||||
super.dispose();
|
||||
_localRenderer.dispose();
|
||||
_remoteRenderer.dispose();
|
||||
_audioButton.close();
|
||||
_videoButton.close();
|
||||
stream.dispose();
|
||||
_disposeStreamsAndSubscriptions();
|
||||
}
|
||||
|
||||
Future<void> _disposeStreamsAndSubscriptions() async {
|
||||
if (_onButtonBarVisibleStreamController != null) await _onButtonBarVisibleStreamController.close();
|
||||
if (_onButtonBarHeightStreamController != null) await _onButtonBarHeightStreamController.close();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// return WillPopScope(
|
||||
// onWillPop: () async => false,
|
||||
// child: Scaffold(
|
||||
// backgroundColor: Colors.black,
|
||||
// // body: ,
|
||||
// ),
|
||||
// );
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: showNoise ? _buildNoiseBox() : buildLayout(),
|
||||
);
|
||||
}
|
||||
|
||||
LayoutBuilder buildLayout() {
|
||||
return LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
return Stack(
|
||||
children: [
|
||||
CamViewWidget(
|
||||
localRenderer: _localRenderer,
|
||||
remoteRenderer: _remoteRenderer,
|
||||
stream: stream,
|
||||
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,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Function _onAudioEnable() {
|
||||
bool enabled = stream.getAudioTracks()[0].enabled;
|
||||
stream.getAudioTracks()[0].enabled = !enabled;
|
||||
_audioButton.add(!enabled);
|
||||
}
|
||||
|
||||
Function _onVideoEnabled() {
|
||||
bool enabled = stream.getVideoTracks()[0].enabled;
|
||||
stream.getVideoTracks()[0].enabled = !enabled;
|
||||
_videoButton.add(!enabled);
|
||||
}
|
||||
|
||||
Function _onSwitchCamera() {
|
||||
// stream.getAudioTracks()[0].enabled = false;
|
||||
// stream.getVideoTracks()[0].enabled = false;
|
||||
Helper.switchCamera(stream.getVideoTracks()[0]);
|
||||
}
|
||||
|
||||
void _onShowBar() {
|
||||
setState(() {
|
||||
SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.bottom, SystemUiOverlay.top]);
|
||||
});
|
||||
_onButtonBarVisibleStreamController.add(true);
|
||||
}
|
||||
|
||||
void _onHeightBar(double height) {
|
||||
_onButtonBarHeightStreamController.add(height);
|
||||
}
|
||||
|
||||
void _onHideBar() {
|
||||
setState(() {
|
||||
SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.bottom]);
|
||||
});
|
||||
_onButtonBarVisibleStreamController.add(false);
|
||||
}
|
||||
|
||||
Future<void> _onHangup() async {
|
||||
print('onHangup');
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,61 @@
|
||||
import 'dart:async';
|
||||
import 'dart:core';
|
||||
|
||||
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 stream;
|
||||
BoxConstraints constraints;
|
||||
StreamController<bool> onButtonBarVisibleStreamController;
|
||||
StreamController<double> onButtonBarHeightStreamController;
|
||||
|
||||
CamViewWidget({this.localRenderer, this.remoteRenderer, this.stream, this.constraints, this.onButtonBarVisibleStreamController, this.onButtonBarHeightStreamController});
|
||||
|
||||
@override
|
||||
_CamViewWidgetState createState() => _CamViewWidgetState();
|
||||
}
|
||||
|
||||
class _CamViewWidgetState extends State<CamViewWidget> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
Future.delayed(const Duration(milliseconds: 300), () {
|
||||
showCamera();
|
||||
});
|
||||
}
|
||||
|
||||
showCamera() async {
|
||||
setState(() async {
|
||||
widget.localRenderer.srcObject = widget.stream;
|
||||
widget.remoteRenderer.srcObject = widget.stream;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
child: Stack(
|
||||
children: [
|
||||
Container(
|
||||
child: RTCVideoView(widget.localRenderer, mirror: true),
|
||||
),
|
||||
DraggableCam(
|
||||
key: Key('publisher'),
|
||||
onButtonBarHeight: widget.onButtonBarHeightStreamController.stream,
|
||||
onButtonBarVisible: widget.onButtonBarVisibleStreamController.stream,
|
||||
availableScreenSize: widget.constraints.biggest,
|
||||
child: RTCVideoView(widget.remoteRenderer),
|
||||
),
|
||||
// Expanded(child: RTCVideoView(widget.remoteRenderer)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,173 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:diplomaticquarterapp/pages/conference/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,
|
||||
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;
|
||||
double _width;
|
||||
double _height;
|
||||
double _top;
|
||||
double _left;
|
||||
double _viewPaddingTop;
|
||||
double _viewPaddingBottom;
|
||||
final double _padding = 8.0;
|
||||
final Duration _duration300ms = const Duration(milliseconds: 300);
|
||||
final Duration _duration0ms = const Duration(milliseconds: 0);
|
||||
Duration _duration;
|
||||
StreamSubscription _streamSubscription;
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue