diff --git a/.github/workflows/flutter.yml b/.github/workflows/flutter.yml index d6ea4a3..3b9b3ae 100644 --- a/.github/workflows/flutter.yml +++ b/.github/workflows/flutter.yml @@ -22,7 +22,7 @@ jobs: java-version: '12.x' - uses: subosito/flutter-action@v1 with: - flutter-version: '2.0.5' + flutter-version: '3.3.2' channel: 'stable' - run: flutter packages get - run: flutter test diff --git a/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java b/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java deleted file mode 100644 index c18798d..0000000 --- a/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java +++ /dev/null @@ -1,20 +0,0 @@ -package io.flutter.plugins; - -import androidx.annotation.Keep; -import androidx.annotation.NonNull; - -import io.flutter.embedding.engine.FlutterEngine; - -/** - * Generated file. Do not edit. - * This file is generated by the Flutter tool based on the - * plugins that support the Android platform. - */ -@Keep -public final class GeneratedPluginRegistrant { - public static void registerWith(@NonNull FlutterEngine flutterEngine) { - flutterEngine.getPlugins().add(new com.cloudwebrtc.webrtc.FlutterWebRTCPlugin()); - flutterEngine.getPlugins().add(new io.flutter.plugins.pathprovider.PathProviderPlugin()); - flutterEngine.getPlugins().add(new io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin()); - } -} diff --git a/ios/Runner/GeneratedPluginRegistrant.h b/ios/Runner/GeneratedPluginRegistrant.h deleted file mode 100644 index ed9a5c6..0000000 --- a/ios/Runner/GeneratedPluginRegistrant.h +++ /dev/null @@ -1,17 +0,0 @@ -// -// Generated file. Do not edit. -// - -#ifndef GeneratedPluginRegistrant_h -#define GeneratedPluginRegistrant_h - -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface GeneratedPluginRegistrant : NSObject -+ (void)registerWithRegistry:(NSObject*)registry; -@end - -NS_ASSUME_NONNULL_END -#endif /* GeneratedPluginRegistrant_h */ diff --git a/ios/Runner/GeneratedPluginRegistrant.m b/ios/Runner/GeneratedPluginRegistrant.m deleted file mode 100644 index 8331b4a..0000000 --- a/ios/Runner/GeneratedPluginRegistrant.m +++ /dev/null @@ -1,33 +0,0 @@ -// -// Generated file. Do not edit. -// - -#import "GeneratedPluginRegistrant.h" - -#if __has_include() -#import -#else -@import flutter_webrtc; -#endif - -#if __has_include() -#import -#else -@import path_provider; -#endif - -#if __has_include() -#import -#else -@import shared_preferences; -#endif - -@implementation GeneratedPluginRegistrant - -+ (void)registerWithRegistry:(NSObject*)registry { - [FlutterWebRTCPlugin registerWithRegistrar:[registry registrarForPlugin:@"FlutterWebRTCPlugin"]]; - [FLTPathProviderPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTPathProviderPlugin"]]; - [FLTSharedPreferencesPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTSharedPreferencesPlugin"]]; -} - -@end diff --git a/lib/src/call_sample/call_sample.dart b/lib/src/call_sample/call_sample.dart index 2c875a7..d0a289b 100644 --- a/lib/src/call_sample/call_sample.dart +++ b/lib/src/call_sample/call_sample.dart @@ -21,7 +21,7 @@ class _CallSampleState extends State { RTCVideoRenderer _remoteRenderer = RTCVideoRenderer(); bool _inCalling = false; Session? _session; - + DesktopCapturerSource? selected_source_; bool _waitAccept = false; // ignore: unused_element @@ -31,7 +31,7 @@ class _CallSampleState extends State { initState() { super.initState(); initRenderers(); - _connect(); + _connect(context); } initRenderers() async { @@ -47,8 +47,8 @@ class _CallSampleState extends State { _remoteRenderer.dispose(); } - void _connect() async { - _signaling ??= Signaling(widget.host)..connect(); + void _connect(BuildContext context) async { + _signaling ??= Signaling(widget.host, context)..connect(); _signaling?.onSignalingStateChange = (SignalingState state) { switch (state) { case SignalingState.ConnectionClosed: diff --git a/lib/src/call_sample/data_channel_sample.dart b/lib/src/call_sample/data_channel_sample.dart index 601729c..aaca560 100644 --- a/lib/src/call_sample/data_channel_sample.dart +++ b/lib/src/call_sample/data_channel_sample.dart @@ -29,7 +29,7 @@ class _DataChannelSampleState extends State { @override initState() { super.initState(); - _connect(); + _connect(context); } @override @@ -39,8 +39,8 @@ class _DataChannelSampleState extends State { _timer?.cancel(); } - void _connect() async { - _signaling ??= Signaling(widget.host)..connect(); + void _connect(BuildContext context) async { + _signaling ??= Signaling(widget.host, context)..connect(); _signaling?.onDataChannelMessage = (_, dc, RTCDataChannelMessage data) { setState(() { diff --git a/lib/src/call_sample/signaling.dart b/lib/src/call_sample/signaling.dart index ff59b7c..f375889 100644 --- a/lib/src/call_sample/signaling.dart +++ b/lib/src/call_sample/signaling.dart @@ -1,7 +1,9 @@ import 'dart:convert'; import 'dart:async'; +import 'package:flutter/material.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart'; +import '../utils/screen_select_dialog.dart'; import 'random_string.dart'; import '../utils/device_info.dart' @@ -39,12 +41,13 @@ class Session { } class Signaling { - Signaling(this._host); + Signaling(this._host, this._context); JsonEncoder _encoder = JsonEncoder(); JsonDecoder _decoder = JsonDecoder(); String _selfId = randomNumeric(6); SimpleWebSocket? _socket; + BuildContext? _context; var _host; var _port = 8086; var _turnCredential; @@ -330,7 +333,8 @@ class Signaling { await _socket?.connect(); } - Future createStream(String media, bool userScreen) async { + Future createStream(String media, bool userScreen, + {BuildContext? context}) async { final Map mediaConstraints = { 'audio': userScreen ? false : true, 'video': userScreen @@ -346,22 +350,43 @@ class Signaling { 'optional': [], } }; + late MediaStream stream; + if (userScreen) { + if (WebRTC.platformIsDesktop) { + final source = await showDialog( + context: context!, + builder: (context) => ScreenSelectDialog(), + ); + stream = await navigator.mediaDevices.getDisplayMedia({ + 'video': source == null + ? true + : { + 'deviceId': {'exact': source.id}, + 'mandatory': {'frameRate': 30.0} + } + }); + } else { + stream = await navigator.mediaDevices.getDisplayMedia(mediaConstraints); + } + } else { + stream = await navigator.mediaDevices.getUserMedia(mediaConstraints); + } - MediaStream stream = userScreen - ? await navigator.mediaDevices.getDisplayMedia(mediaConstraints) - : await navigator.mediaDevices.getUserMedia(mediaConstraints); onLocalStream?.call(stream); return stream; } - Future _createSession(Session? session, - {required String peerId, - required String sessionId, - required String media, - required bool screenSharing}) async { + Future _createSession( + Session? session, { + required String peerId, + required String sessionId, + required String media, + required bool screenSharing, + }) async { var newSession = session ?? Session(sid: sessionId, pid: peerId); if (media != 'data') - _localStream = await createStream(media, screenSharing); + _localStream = + await createStream(media, screenSharing, context: _context); print(_iceServers); RTCPeerConnection pc = await createPeerConnection({ ..._iceServers, diff --git a/lib/src/utils/screen_select_dialog.dart b/lib/src/utils/screen_select_dialog.dart new file mode 100644 index 0000000..45a4f2a --- /dev/null +++ b/lib/src/utils/screen_select_dialog.dart @@ -0,0 +1,307 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_webrtc/flutter_webrtc.dart'; + +class ThumbnailWidget extends StatefulWidget { + const ThumbnailWidget( + {Key? key, + required this.source, + required this.selected, + required this.onTap}) + : super(key: key); + final DesktopCapturerSource source; + final bool selected; + final Function(DesktopCapturerSource) onTap; + + @override + _ThumbnailWidgetState createState() => _ThumbnailWidgetState(); +} + +class _ThumbnailWidgetState extends State { + final List _subscriptions = []; + + @override + void initState() { + super.initState(); + _subscriptions.add(widget.source.onThumbnailChanged.stream.listen((event) { + setState(() {}); + })); + _subscriptions.add(widget.source.onNameChanged.stream.listen((event) { + setState(() {}); + })); + } + + @override + void deactivate() { + _subscriptions.forEach((element) { + element.cancel(); + }); + super.deactivate(); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Expanded( + child: Container( + decoration: widget.selected + ? BoxDecoration( + border: Border.all(width: 2, color: Colors.blueAccent)) + : null, + child: InkWell( + onTap: () { + print('Selected source id => ${widget.source.id}'); + widget.onTap(widget.source); + }, + child: widget.source.thumbnail != null + ? Image.memory( + widget.source.thumbnail!, + gaplessPlayback: true, + alignment: Alignment.center, + ) + : Container(), + ), + )), + Text( + widget.source.name, + style: TextStyle( + fontSize: 12, + color: Colors.black87, + fontWeight: + widget.selected ? FontWeight.bold : FontWeight.normal), + ), + ], + ); + } +} + +// ignore: must_be_immutable +class ScreenSelectDialog extends Dialog { + ScreenSelectDialog() { + Future.delayed(Duration(milliseconds: 100), () { + _getSources(); + }); + _subscriptions.add(desktopCapturer.onAdded.stream.listen((source) { + _sources[source.id] = source; + _stateSetter?.call(() {}); + })); + + _subscriptions.add(desktopCapturer.onRemoved.stream.listen((source) { + _sources.remove(source.id); + _stateSetter?.call(() {}); + })); + + _subscriptions + .add(desktopCapturer.onThumbnailChanged.stream.listen((source) { + _stateSetter?.call(() {}); + })); + } + final Map _sources = {}; + SourceType _sourceType = SourceType.Screen; + DesktopCapturerSource? _selected_source; + final List> _subscriptions = []; + StateSetter? _stateSetter; + Timer? _timer; + + void _ok(context) async { + _timer?.cancel(); + _subscriptions.forEach((element) { + element.cancel(); + }); + Navigator.pop(context, _selected_source); + } + + void _cancel(context) async { + _timer?.cancel(); + _subscriptions.forEach((element) { + element.cancel(); + }); + Navigator.pop(context, null); + } + + Future _getSources() async { + try { + var sources = await desktopCapturer.getSources(types: [_sourceType]); + sources.forEach((element) { + print( + 'name: ${element.name}, id: ${element.id}, type: ${element.type}'); + }); + _timer?.cancel(); + _timer = Timer.periodic(Duration(seconds: 3), (timer) { + desktopCapturer.updateSources(types: [_sourceType]); + }); + _sources.clear(); + sources.forEach((element) { + _sources[element.id] = element; + }); + _stateSetter?.call(() {}); + return; + } catch (e) { + print(e.toString()); + } + } + + @override + Widget build(BuildContext context) { + return Material( + type: MaterialType.transparency, + child: Center( + child: Container( + width: 640, + height: 560, + color: Colors.white, + child: Column( + children: [ + Padding( + padding: EdgeInsets.all(10), + child: Stack( + children: [ + Align( + alignment: Alignment.topLeft, + child: Text( + 'Choose what to share', + style: TextStyle(fontSize: 16, color: Colors.black87), + ), + ), + Align( + alignment: Alignment.topRight, + child: InkWell( + child: Icon(Icons.close), + onTap: () => _cancel(context), + ), + ), + ], + ), + ), + Expanded( + flex: 1, + child: Container( + width: double.infinity, + padding: EdgeInsets.all(10), + child: StatefulBuilder( + builder: (context, setState) { + _stateSetter = setState; + return DefaultTabController( + length: 2, + child: Column( + children: [ + Container( + constraints: BoxConstraints.expand(height: 24), + child: TabBar( + onTap: (value) => Future.delayed( + Duration(milliseconds: 300), () { + _sourceType = value == 0 + ? SourceType.Screen + : SourceType.Window; + _getSources(); + }), + tabs: [ + Tab( + child: Text( + 'Entire Screen', + style: TextStyle(color: Colors.black54), + )), + Tab( + child: Text( + 'Window', + style: TextStyle(color: Colors.black54), + )), + ]), + ), + SizedBox( + height: 2, + ), + Expanded( + child: Container( + child: TabBarView(children: [ + Align( + alignment: Alignment.center, + child: Container( + child: GridView.count( + crossAxisSpacing: 8, + crossAxisCount: 2, + children: _sources.entries + .where((element) => + element.value.type == + SourceType.Screen) + .map((e) => ThumbnailWidget( + onTap: (source) { + setState(() { + _selected_source = source; + }); + }, + source: e.value, + selected: + _selected_source?.id == + e.value.id, + )) + .toList(), + ), + )), + Align( + alignment: Alignment.center, + child: Container( + child: GridView.count( + crossAxisSpacing: 8, + crossAxisCount: 3, + children: _sources.entries + .where((element) => + element.value.type == + SourceType.Window) + .map((e) => ThumbnailWidget( + onTap: (source) { + setState(() { + _selected_source = source; + }); + }, + source: e.value, + selected: + _selected_source?.id == + e.value.id, + )) + .toList(), + ), + )), + ]), + ), + ) + ], + ), + ); + }, + ), + ), + ), + Container( + width: double.infinity, + child: ButtonBar( + children: [ + MaterialButton( + child: Text( + 'Cancel', + style: TextStyle(color: Colors.black54), + ), + onPressed: () { + _cancel(context); + }, + ), + MaterialButton( + color: Theme.of(context).primaryColor, + child: Text( + 'Share', + ), + onPressed: () { + _ok(context); + }, + ), + ], + ), + ), + ], + ), + )), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 360f7d8..b1c4a07 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: A new Flutter application. version: 1.2.0 environment: - sdk: '>=2.12.0 <3.0.0' + sdk: '>=2.15.0 <3.0.0' flutter: '>=1.22.0' dependencies: