From a554d92543e87ce985bcb1a0c74baaf6455ff055 Mon Sep 17 00:00:00 2001 From: CloudWebRTC Date: Tue, 2 Aug 2022 11:17:09 +0800 Subject: [PATCH] screen sharing for desktop. (#135) * screen sharing for desktop (WIP). * use git repo for testing. * update. * Add sourceId/frameRate to ScreenShareCaptureOptions. * Compatible with flutter web. * Rendering of camera and screenshare video tracks at same time. * Remote screen sharing is shown first, and fixed sub/unsub button for screenshare widget. * resolved conflict. * modify flutter-webrtc to pub version. * remove unused import. * chore: Common params for Camera and ScreenShare. --- example/lib/pages/room.dart | 77 +++-- example/lib/widgets/controls.dart | 29 +- example/lib/widgets/participant.dart | 87 ++--- example/lib/widgets/participant_info.dart | 37 +- example/macos/Podfile.lock | 14 +- example/pubspec.lock | 2 +- lib/livekit_client.dart | 3 +- lib/src/track/options.dart | 63 +++- lib/src/widgets/screen_select_dialog.dart | 318 ++++++++++++++++++ .../video_track_renderer.dart | 0 macos/livekit_client.podspec | 2 +- pubspec.lock | 2 +- pubspec.yaml | 2 +- 13 files changed, 533 insertions(+), 103 deletions(-) create mode 100644 lib/src/widgets/screen_select_dialog.dart rename lib/src/{widget => widgets}/video_track_renderer.dart (100%) diff --git a/example/lib/pages/room.dart b/example/lib/pages/room.dart index 781c6bbe..74dfca33 100644 --- a/example/lib/pages/room.dart +++ b/example/lib/pages/room.dart @@ -7,6 +7,7 @@ import 'package:livekit_client/livekit_client.dart'; import '../exts.dart'; import '../widgets/controls.dart'; import '../widgets/participant.dart'; +import '../widgets/participant_info.dart'; class RoomPage extends StatefulWidget { // @@ -25,7 +26,7 @@ class RoomPage extends StatefulWidget { class _RoomPageState extends State { // - List participants = []; + List participantTracks = []; EventsListener get _listener => widget.listener; bool get fastConnection => widget.room.engine.fastConnectOptions != null; @override @@ -57,6 +58,8 @@ class _RoomPageState extends State { WidgetsBindingCompatible.instance ?.addPostFrameCallback((timeStamp) => Navigator.pop(context)); }) + ..on((_) => _sortParticipants()) + ..on((_) => _sortParticipants()) ..on((event) { String decoded = 'Failed to decode'; try { @@ -90,13 +93,30 @@ class _RoomPageState extends State { } void _sortParticipants() { - List participants = []; - participants.addAll(widget.room.participants.values); + List userMediaTracks = []; + List screenTracks = []; + for (var participant in widget.room.participants.values) { + for (var t in participant.videoTracks) { + if (t.isScreenShare) { + screenTracks.add(ParticipantTrack( + participant: participant, + videoTrack: t.track, + isScreenShare: true, + )); + } else { + userMediaTracks.add(ParticipantTrack( + participant: participant, + videoTrack: t.track, + isScreenShare: false, + )); + } + } + } // sort speakers for the grid - participants.sort((a, b) { + userMediaTracks.sort((a, b) { // loudest speaker first - if (a.isSpeaking && b.isSpeaking) { - if (a.audioLevel > b.audioLevel) { + if (a.participant.isSpeaking && b.participant.isSpeaking) { + if (a.participant.audioLevel > b.participant.audioLevel) { return -1; } else { return 1; @@ -104,33 +124,43 @@ class _RoomPageState extends State { } // last spoken at - final aSpokeAt = a.lastSpokeAt?.millisecondsSinceEpoch ?? 0; - final bSpokeAt = b.lastSpokeAt?.millisecondsSinceEpoch ?? 0; + final aSpokeAt = a.participant.lastSpokeAt?.millisecondsSinceEpoch ?? 0; + final bSpokeAt = b.participant.lastSpokeAt?.millisecondsSinceEpoch ?? 0; if (aSpokeAt != bSpokeAt) { return aSpokeAt > bSpokeAt ? -1 : 1; } // video on - if (a.hasVideo != b.hasVideo) { - return a.hasVideo ? -1 : 1; + if (a.participant.hasVideo != b.participant.hasVideo) { + return a.participant.hasVideo ? -1 : 1; } // joinedAt - return a.joinedAt.millisecondsSinceEpoch - - b.joinedAt.millisecondsSinceEpoch; + return a.participant.joinedAt.millisecondsSinceEpoch - + b.participant.joinedAt.millisecondsSinceEpoch; }); - final localParticipant = widget.room.localParticipant; - if (localParticipant != null) { - if (participants.length > 1) { - participants.insert(1, localParticipant); - } else { - participants.add(localParticipant); + final localParticipantTracks = widget.room.localParticipant?.videoTracks; + if (localParticipantTracks != null) { + for (var t in localParticipantTracks) { + if (t.isScreenShare) { + screenTracks.add(ParticipantTrack( + participant: widget.room.localParticipant!, + videoTrack: t.track, + isScreenShare: true, + )); + } else { + userMediaTracks.add(ParticipantTrack( + participant: widget.room.localParticipant!, + videoTrack: t.track, + isScreenShare: false, + )); + } } } setState(() { - this.participants = participants; + participantTracks = [...screenTracks, ...userMediaTracks]; }); } @@ -139,18 +169,19 @@ class _RoomPageState extends State { body: Column( children: [ Expanded( - child: participants.isNotEmpty - ? ParticipantWidget.widgetFor(participants.first) + child: participantTracks.isNotEmpty + ? ParticipantWidget.widgetFor(participantTracks.first) : Container()), SizedBox( height: 100, child: ListView.builder( scrollDirection: Axis.horizontal, - itemCount: math.max(0, participants.length - 1), + itemCount: math.max(0, participantTracks.length - 1), itemBuilder: (BuildContext context, int index) => SizedBox( width: 100, height: 100, - child: ParticipantWidget.widgetFor(participants[index + 1]), + child: + ParticipantWidget.widgetFor(participantTracks[index + 1]), ), ), ), diff --git a/example/lib/widgets/controls.dart b/example/lib/widgets/controls.dart index 690f6ad4..b19349ad 100644 --- a/example/lib/widgets/controls.dart +++ b/example/lib/widgets/controls.dart @@ -6,6 +6,7 @@ import 'package:eva_icons_flutter/eva_icons_flutter.dart'; import 'package:flutter/material.dart'; import 'package:flutter_background/flutter_background.dart'; import 'package:livekit_client/livekit_client.dart'; +import 'package:flutter_webrtc/flutter_webrtc.dart'; import '../exts.dart'; @@ -86,9 +87,30 @@ class _ControlsWidgetState extends State { } void _enableScreenShare() async { - await participant.setScreenShareEnabled(true); - - if (Platform.isAndroid) { + if (WebRTC.platformIsDesktop) { + try { + final source = await showDialog( + context: context, + builder: (context) => ScreenSelectDialog(), + ); + if (source == null) { + print('cancelled screenshare'); + return; + } + print('DesktopCapturerSource: ${source.id}'); + var track = await LocalVideoTrack.createScreenShareTrack( + ScreenShareCaptureOptions( + sourceId: source.id, + maxFrameRate: 15.0, + ), + ); + await participant.publishVideoTrack(track); + } catch (e) { + print('could not publish video: $e'); + } + return; + } + if (WebRTC.platformIsAndroid) { // Android specific try { // Required for android screenshare. @@ -105,6 +127,7 @@ class _ControlsWidgetState extends State { print('could not publish video: $e'); } } + await participant.setScreenShareEnabled(true); } void _disableScreenShare() async { diff --git a/example/lib/widgets/participant.dart b/example/lib/widgets/participant.dart index a8b3e979..8e23cd8b 100644 --- a/example/lib/widgets/participant.dart +++ b/example/lib/widgets/participant.dart @@ -10,17 +10,25 @@ import 'participant_info.dart'; abstract class ParticipantWidget extends StatefulWidget { // Convenience method to return relevant widget for participant - static ParticipantWidget widgetFor(Participant participant) { - if (participant is LocalParticipant) { - return LocalParticipantWidget(participant); - } else if (participant is RemoteParticipant) { - return RemoteParticipantWidget(participant); + static ParticipantWidget widgetFor(ParticipantTrack participantTrack) { + if (participantTrack.participant is LocalParticipant) { + return LocalParticipantWidget( + participantTrack.participant as LocalParticipant, + participantTrack.videoTrack, + participantTrack.isScreenShare); + } else if (participantTrack.participant is RemoteParticipant) { + return RemoteParticipantWidget( + participantTrack.participant as RemoteParticipant, + participantTrack.videoTrack, + participantTrack.isScreenShare); } throw UnimplementedError('Unknown participant type'); } // Must be implemented by child class abstract final Participant participant; + abstract final VideoTrack? videoTrack; + abstract final bool isScreenShare; final VideoQuality quality; const ParticipantWidget({ @@ -32,9 +40,15 @@ abstract class ParticipantWidget extends StatefulWidget { class LocalParticipantWidget extends ParticipantWidget { @override final LocalParticipant participant; + @override + final VideoTrack? videoTrack; + @override + final bool isScreenShare; const LocalParticipantWidget( - this.participant, { + this.participant, + this.videoTrack, + this.isScreenShare, { Key? key, }) : super(key: key); @@ -45,9 +59,15 @@ class LocalParticipantWidget extends ParticipantWidget { class RemoteParticipantWidget extends ParticipantWidget { @override final RemoteParticipant participant; + @override + final VideoTrack? videoTrack; + @override + final bool isScreenShare; const RemoteParticipantWidget( - this.participant, { + this.participant, + this.videoTrack, + this.isScreenShare, { Key? key, }) : super(key: key); @@ -60,7 +80,7 @@ abstract class _ParticipantWidgetState // bool _visible = true; VideoTrack? get activeVideoTrack; - TrackPublication? get firstVideoPublication; + TrackPublication? get videoPublication; TrackPublication? get firstAudioPublication; @override @@ -89,12 +109,12 @@ abstract class _ParticipantWidgetState void _onParticipantChanged() => setState(() {}); // Widgets to show above the info bar - List extraWidgets() => []; + List extraWidgets(bool isScreenShare) => []; @override Widget build(BuildContext ctx) => Container( foregroundDecoration: BoxDecoration( - border: widget.participant.isSpeaking + border: widget.participant.isSpeaking && !widget.isScreenShare ? Border.all( width: 5, color: LKColors.lkBlue, @@ -112,7 +132,7 @@ abstract class _ParticipantWidgetState child: activeVideoTrack != null ? VideoTrackRenderer( activeVideoTrack!, - fit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover, + fit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain, ) : const NoVideoWidget(), ), @@ -124,7 +144,7 @@ abstract class _ParticipantWidgetState crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ - ...extraWidgets(), + ...extraWidgets(widget.isScreenShare), ParticipantInfoWidget( title: widget.participant.name.isNotEmpty ? '${widget.participant.name} (${widget.participant.identity})' @@ -132,6 +152,7 @@ abstract class _ParticipantWidgetState audioAvailable: firstAudioPublication?.muted == false && firstAudioPublication?.subscribed == true, connectionQuality: widget.participant.connectionQuality, + isScreenShare: widget.isScreenShare, ), ], ), @@ -144,60 +165,48 @@ abstract class _ParticipantWidgetState class _LocalParticipantWidgetState extends _ParticipantWidgetState { @override - LocalTrackPublication? get firstVideoPublication => - widget.participant.videoTracks.firstOrNull; + LocalTrackPublication? get videoPublication => + widget.participant.videoTracks + .where((element) => element.sid == widget.videoTrack?.sid) + .firstOrNull; @override LocalTrackPublication? get firstAudioPublication => widget.participant.audioTracks.firstOrNull; @override - VideoTrack? get activeVideoTrack { - if (firstVideoPublication?.subscribed == true && - firstVideoPublication?.muted == false && - _visible) { - return firstVideoPublication?.track; - } - return null; - } + VideoTrack? get activeVideoTrack => widget.videoTrack; } class _RemoteParticipantWidgetState extends _ParticipantWidgetState { @override - RemoteTrackPublication? get firstVideoPublication => - widget.participant.videoTracks.firstOrNull; + RemoteTrackPublication? get videoPublication => + widget.participant.videoTracks + .where((element) => element.sid == widget.videoTrack?.sid) + .firstOrNull; @override RemoteTrackPublication? get firstAudioPublication => widget.participant.audioTracks.firstOrNull; @override - VideoTrack? get activeVideoTrack { - for (final trackPublication in widget.participant.videoTracks) { - print( - 'video track ${trackPublication.sid} subscribed ${trackPublication.subscribed} muted ${trackPublication.muted}'); - if (trackPublication.subscribed && !trackPublication.muted && _visible) { - return trackPublication.track; - } - } - return null; - } + VideoTrack? get activeVideoTrack => widget.videoTrack; @override - List extraWidgets() => [ + List extraWidgets(bool isScreenShare) => [ Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.end, children: [ // Menu for RemoteTrackPublication - if (firstVideoPublication != null) + if (videoPublication != null) RemoteTrackPublicationMenuWidget( - pub: firstVideoPublication!, - icon: EvaIcons.video, + pub: videoPublication!, + icon: isScreenShare ? EvaIcons.monitor : EvaIcons.video, ), // Menu for RemoteTrackPublication - if (firstAudioPublication != null) + if (firstAudioPublication != null && !isScreenShare) RemoteTrackPublicationMenuWidget( pub: firstAudioPublication!, icon: EvaIcons.volumeUp, diff --git a/example/lib/widgets/participant_info.dart b/example/lib/widgets/participant_info.dart index 56e822dd..b1b0035c 100644 --- a/example/lib/widgets/participant_info.dart +++ b/example/lib/widgets/participant_info.dart @@ -2,16 +2,28 @@ import 'package:eva_icons_flutter/eva_icons_flutter.dart'; import 'package:flutter/material.dart'; import 'package:livekit_client/livekit_client.dart'; +class ParticipantTrack { + ParticipantTrack( + {required this.participant, + required this.videoTrack, + required this.isScreenShare}); + VideoTrack? videoTrack; + Participant participant; + final bool isScreenShare; +} + class ParticipantInfoWidget extends StatelessWidget { // final String? title; final bool audioAvailable; final ConnectionQuality connectionQuality; + final bool isScreenShare; const ParticipantInfoWidget({ this.title, this.audioAvailable = true, this.connectionQuality = ConnectionQuality.unknown, + this.isScreenShare = false, Key? key, }) : super(key: key); @@ -33,14 +45,23 @@ class ParticipantInfoWidget extends StatelessWidget { overflow: TextOverflow.ellipsis, ), ), - Padding( - padding: const EdgeInsets.only(left: 5), - child: Icon( - audioAvailable ? EvaIcons.mic : EvaIcons.micOff, - color: audioAvailable ? Colors.white : Colors.red, - size: 16, - ), - ), + isScreenShare + ? const Padding( + padding: EdgeInsets.only(left: 5), + child: Icon( + EvaIcons.monitor, + color: Colors.white, + size: 16, + ), + ) + : Padding( + padding: const EdgeInsets.only(left: 5), + child: Icon( + audioAvailable ? EvaIcons.mic : EvaIcons.micOff, + color: audioAvailable ? Colors.white : Colors.red, + size: 16, + ), + ), if (connectionQuality != ConnectionQuality.unknown) Padding( padding: const EdgeInsets.only(left: 5), diff --git a/example/macos/Podfile.lock b/example/macos/Podfile.lock index 5f47274d..235241ab 100644 --- a/example/macos/Podfile.lock +++ b/example/macos/Podfile.lock @@ -1,18 +1,18 @@ PODS: - device_info_plus_macos (0.0.1): - FlutterMacOS - - flutter_webrtc (0.7.1): + - flutter_webrtc (0.9.0): - FlutterMacOS - - WebRTC-SDK (= 97.4692.06) + - WebRTC-SDK (= 97.4692.07) - FlutterMacOS (1.0.0) - livekit_client (1.0.0): - FlutterMacOS - - WebRTC-SDK (~> 97.4692) + - WebRTC-SDK (~> 97.4692.07) - path_provider_macos (0.0.1): - FlutterMacOS - shared_preferences_macos (0.0.1): - FlutterMacOS - - WebRTC-SDK (97.4692.06) + - WebRTC-SDK (97.4692.07) DEPENDENCIES: - device_info_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus_macos/macos`) @@ -42,12 +42,12 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: device_info_plus_macos: 1ad388a1ef433505c4038e7dd9605aadd1e2e9c7 - flutter_webrtc: a019f551482678a0341c79deacbf8d14ab241ceb + flutter_webrtc: 708996d57dcf06da1c6a8e820a43112618d62213 FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424 - livekit_client: 7479527f38e9c4a4dc2d3c58f99ab0dd531d24d6 + livekit_client: 758f6a39a322a64e2081253b89c66d6f5275816f path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19 shared_preferences_macos: a64dc611287ed6cbe28fd1297898db1336975727 - WebRTC-SDK: eca7a6620a3897801207803672b3d036fb40b9d5 + WebRTC-SDK: e16ed5e9f54d2a983d5f1f479a4b55f20da202fe PODFILE CHECKSUM: 6eac6b3292e5142cfc23bdeb71848a40ec51c14c diff --git a/example/pubspec.lock b/example/pubspec.lock index 4ed34af8..411e8ae5 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -176,7 +176,7 @@ packages: name: flutter_webrtc url: "https://pub.dartlang.org" source: hosted - version: "0.8.12" + version: "0.9.0" google_fonts: dependency: "direct main" description: diff --git a/lib/livekit_client.dart b/lib/livekit_client.dart index 81fcf627..6d75931b 100644 --- a/lib/livekit_client.dart +++ b/lib/livekit_client.dart @@ -28,5 +28,6 @@ export 'src/types/participant_permissions.dart'; export 'src/types/video_dimensions.dart'; export 'src/types/video_encoding.dart'; export 'src/types/video_parameters.dart'; -export 'src/widget/video_track_renderer.dart'; +export 'src/widgets/screen_select_dialog.dart'; +export 'src/widgets/video_track_renderer.dart'; export 'src/extensions.dart' show WidgetsBindingCompatible; diff --git a/lib/src/track/options.dart b/lib/src/track/options.dart index 2d661294..d4120faa 100644 --- a/lib/src/track/options.dart +++ b/lib/src/track/options.dart @@ -1,6 +1,5 @@ -import 'dart:io'; - import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:flutter_webrtc/flutter_webrtc.dart'; import '../track/local/audio.dart'; import '../track/local/video.dart'; @@ -23,26 +22,22 @@ extension CameraPositionExt on CameraPosition { /// Options used when creating a [LocalVideoTrack] that captures the camera. class CameraCaptureOptions extends VideoCaptureOptions { - /// The deviceId of the capture device to use. - /// Available deviceIds can be obtained through `flutter_webrtc`: - ///
-  /// import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
-  ///
-  /// List devices = await rtc.navigator.mediaDevices.enumerateDevices();
-  /// 
- final String? deviceId; final CameraPosition cameraPosition; const CameraCaptureOptions({ - this.deviceId, this.cameraPosition = CameraPosition.front, + String? deviceId, + double? maxFrameRate, VideoParameters params = VideoParametersPresets.h540_169, - }) : super(params: params); + }) : super(params: params, deviceId: deviceId, maxFrameRate: maxFrameRate); CameraCaptureOptions.from({required VideoCaptureOptions captureOptions}) - : deviceId = null, - cameraPosition = CameraPosition.front, - super(params: captureOptions.params); + : cameraPosition = CameraPosition.front, + super( + params: captureOptions.params, + deviceId: captureOptions.deviceId, + maxFrameRate: captureOptions.maxFrameRate, + ); @override Map toMediaConstraintsMap() { @@ -60,6 +55,9 @@ class CameraCaptureOptions extends VideoCaptureOptions { ]; } } + if (maxFrameRate != null) { + constraints['frameRate'] = {'max': maxFrameRate}; + } return constraints; } @@ -67,21 +65,26 @@ class CameraCaptureOptions extends VideoCaptureOptions { CameraCaptureOptions copyWith({ VideoParameters? params, CameraPosition? cameraPosition, + String? deviceId, + double? maxFrameRate, }) => CameraCaptureOptions( params: params ?? this.params, cameraPosition: cameraPosition ?? this.cameraPosition, + deviceId: deviceId ?? this.deviceId, + maxFrameRate: maxFrameRate ?? this.maxFrameRate, ); } /// Options used when creating a [LocalVideoTrack] that captures the screen. class ScreenShareCaptureOptions extends VideoCaptureOptions { final bool useiOSBroadcastExtension; - const ScreenShareCaptureOptions({ this.useiOSBroadcastExtension = false, + String? sourceId, + double? maxFrameRate, VideoParameters params = VideoParametersPresets.screenShareH720FPS15, - }) : super(params: params); + }) : super(params: params, deviceId: sourceId, maxFrameRate: maxFrameRate); ScreenShareCaptureOptions.from( {this.useiOSBroadcastExtension = false, @@ -91,9 +94,17 @@ class ScreenShareCaptureOptions extends VideoCaptureOptions { @override Map toMediaConstraintsMap() { var constraints = super.toMediaConstraintsMap(); - if (useiOSBroadcastExtension && Platform.isIOS) { + if (useiOSBroadcastExtension && WebRTC.platformIsIOS) { constraints['deviceId'] = 'broadcast'; } + if (WebRTC.platformIsDesktop) { + if (deviceId != null) { + constraints['deviceId'] = {'exact': deviceId}; + } + if (maxFrameRate != 0.0) { + constraints['mandatory'] = {'frameRate': maxFrameRate}; + } + } return constraints; } } @@ -111,8 +122,24 @@ abstract class VideoCaptureOptions extends LocalTrackOptions { // final LocalVideoTrackType type; final VideoParameters params; + /// The deviceId of the capture device to use. + /// Available deviceIds can be obtained through `flutter_webrtc`: + ///
+  /// import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
+  ///
+  /// List devices = await rtc.navigator.mediaDevices.enumerateDevices();
+  /// // or
+  /// List desktopSources = await rtc.desktopCapturer.getSources(types: [rtc.SourceType.Screen, rtc.SourceType.Window]);
+  /// 
+ final String? deviceId; + + // Limit the maximum frameRate of the capture device. + final double? maxFrameRate; + const VideoCaptureOptions({ this.params = VideoParametersPresets.h540_169, + this.deviceId, + this.maxFrameRate, }); @override diff --git a/lib/src/widgets/screen_select_dialog.dart b/lib/src/widgets/screen_select_dialog.dart new file mode 100644 index 00000000..c1d0ab14 --- /dev/null +++ b/lib/src/widgets/screen_select_dialog.dart @@ -0,0 +1,318 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc; + +import '../logger.dart'; + +// ignore: must_be_immutable +class ScreenSelectDialog extends Dialog { + ScreenSelectDialog({Key? key}) : super(key: key) { + Future.delayed(const Duration(milliseconds: 100), () { + _getSources(); + }); + _subscriptions.add(rtc.desktopCapturer.onAdded.stream.listen((source) { + _sources[source.id] = source; + _stateSetter?.call(() {}); + })); + + _subscriptions.add(rtc.desktopCapturer.onRemoved.stream.listen((source) { + _sources.remove(source.id); + _stateSetter?.call(() {}); + })); + + _subscriptions + .add(rtc.desktopCapturer.onNameChanged.stream.listen((source) { + _sources[source.id] = source; + _stateSetter?.call(() {}); + })); + + _subscriptions + .add(rtc.desktopCapturer.onThumbnailChanged.stream.listen((source) { + _sources[source.id] = source; + _stateSetter?.call(() {}); + })); + } + final Map _sources = {}; + rtc.SourceType _sourceType = rtc.SourceType.Screen; + rtc.DesktopCapturerSource? _selectedSource; + final List> _subscriptions = []; + StateSetter? _stateSetter; + Timer? _timer; + + void _ok(BuildContext context) { + _timer?.cancel(); + for (var item in _subscriptions) { + item.cancel(); + } + Navigator.pop(context, _selectedSource); + } + + void _cancel(BuildContext context) { + _timer?.cancel(); + for (var item in _subscriptions) { + item.cancel(); + } + Navigator.pop(context, null); + } + + Future _getSources() async { + try { + var sources = await rtc.desktopCapturer.getSources(types: [_sourceType]); + for (var item in sources) { + logger.info('name: ${item.name}, id: ${item.id}, type: ${item.type}'); + } + _stateSetter?.call(() { + for (var item in sources) { + _sources[item.id] = item; + } + }); + _timer?.cancel(); + _timer = Timer.periodic(const Duration(seconds: 2), (timer) { + rtc.desktopCapturer.updateSources(types: [_sourceType]); + }); + return; + } catch (e) { + logger.warning(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: const EdgeInsets.all(10), + child: Stack( + children: [ + const Align( + alignment: Alignment.topLeft, + child: Text( + 'Choose what to share', + style: TextStyle(fontSize: 16, color: Colors.black87), + ), + ), + Align( + alignment: Alignment.topRight, + child: InkWell( + child: const Icon(Icons.close), + onTap: () => _cancel(context), + ), + ), + ], + ), + ), + Expanded( + flex: 1, + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(10), + child: StatefulBuilder( + builder: (context, setState) { + _stateSetter = setState; + return DefaultTabController( + length: 2, + child: Column( + children: [ + Container( + constraints: + const BoxConstraints.expand(height: 24), + child: TabBar( + onTap: (value) => Future.delayed( + const Duration(milliseconds: 300), () { + _sourceType = value == 0 + ? rtc.SourceType.Screen + : rtc.SourceType.Window; + _getSources(); + }), + tabs: const [ + Tab( + child: Text( + 'Entire Screen', + style: TextStyle(color: Colors.black54), + )), + Tab( + child: Text( + 'Window', + style: TextStyle(color: Colors.black54), + )), + ]), + ), + const SizedBox( + height: 2, + ), + Expanded( + child: TabBarView(children: [ + Align( + alignment: Alignment.center, + child: GridView.count( + crossAxisSpacing: 8, + crossAxisCount: 2, + children: _sources.entries + .where((element) => + element.value.type == + rtc.SourceType.Screen) + .map((e) => Column( + children: [ + Expanded( + child: Container( + decoration: (_selectedSource != + null && + _selectedSource!.id == + e.value.id) + ? BoxDecoration( + border: Border.all( + width: 2, + color: Colors + .blueAccent)) + : null, + child: InkWell( + onTap: () { + logger.info( + 'Selected screen id => ${e.value.id}'); + setState(() { + _selectedSource = + e.value; + }); + }, + child: e.value.thumbnail != + null + ? Image.memory( + e.value.thumbnail!, + scale: 1.0, + repeat: ImageRepeat + .noRepeat, + ) + : Container(), + ), + )), + Text( + e.value.name, + style: TextStyle( + fontSize: 12, + color: Colors.black87, + fontWeight: + (_selectedSource != + null && + _selectedSource! + .id == + e.value + .id) + ? FontWeight.bold + : FontWeight + .normal), + ), + ], + )) + .toList(), + )), + Align( + alignment: Alignment.center, + child: GridView.count( + crossAxisSpacing: 8, + crossAxisCount: 3, + children: _sources.entries + .where((element) => + element.value.type == + rtc.SourceType.Window) + .map((e) => Column( + children: [ + Expanded( + child: Container( + decoration: (_selectedSource != + null && + _selectedSource!.id == + e.value.id) + ? BoxDecoration( + border: Border.all( + width: 2, + color: Colors + .blueAccent)) + : null, + child: InkWell( + onTap: () { + logger.info( + 'Selected window id => ${e.value.id}'); + setState(() { + _selectedSource = + e.value; + }); + }, + child: e.value.thumbnail! + .isNotEmpty + ? Image.memory( + e.value.thumbnail!, + scale: 1.0, + repeat: ImageRepeat + .noRepeat, + ) + : Container(), + ), + )), + Text( + e.value.name, + style: TextStyle( + fontSize: 12, + color: Colors.black87, + fontWeight: + (_selectedSource != + null && + _selectedSource! + .id == + e.value + .id) + ? FontWeight.bold + : FontWeight + .normal), + ), + ], + )) + .toList(), + )), + ]), + ) + ], + ), + ); + }, + ), + ), + ), + SizedBox( + width: double.infinity, + child: ButtonBar( + children: [ + MaterialButton( + child: const Text( + 'Cancel', + style: TextStyle(color: Colors.black54), + ), + onPressed: () { + _cancel(context); + }, + ), + MaterialButton( + color: Theme.of(context).primaryColor, + child: const Text( + 'Share', + ), + onPressed: () { + _ok(context); + }, + ), + ], + ), + ), + ], + ), + )), + ); + } +} diff --git a/lib/src/widget/video_track_renderer.dart b/lib/src/widgets/video_track_renderer.dart similarity index 100% rename from lib/src/widget/video_track_renderer.dart rename to lib/src/widgets/video_track_renderer.dart diff --git a/macos/livekit_client.podspec b/macos/livekit_client.podspec index 6062fead..041dc692 100644 --- a/macos/livekit_client.podspec +++ b/macos/livekit_client.podspec @@ -16,5 +16,5 @@ Pod::Spec.new do |s| s.static_framework = true s.dependency 'FlutterMacOS' - s.dependency 'WebRTC-SDK', '~> 97.4692' + s.dependency 'WebRTC-SDK', '~> 97.4692.07' end diff --git a/pubspec.lock b/pubspec.lock index f2ad67e8..903e151f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -218,7 +218,7 @@ packages: name: flutter_webrtc url: "https://pub.dartlang.org" source: hosted - version: "0.8.10" + version: "0.9.0" glob: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 776af252..78b661cf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -22,7 +22,7 @@ dependencies: uuid: ^3.0.6 synchronized: ^3.0.0 protobuf: ^2.0.1 - flutter_webrtc: ^0.8.12 + flutter_webrtc: ^0.9.0 dart_webrtc: ^1.0.6 device_info_plus: ^3.2.3