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.
This commit is contained in:
CloudWebRTC
2022-08-02 11:17:09 +08:00
committed by GitHub
parent c7722718dd
commit a554d92543
13 changed files with 533 additions and 103 deletions
+54 -23
View File
@@ -7,6 +7,7 @@ import 'package:livekit_client/livekit_client.dart';
import '../exts.dart'; import '../exts.dart';
import '../widgets/controls.dart'; import '../widgets/controls.dart';
import '../widgets/participant.dart'; import '../widgets/participant.dart';
import '../widgets/participant_info.dart';
class RoomPage extends StatefulWidget { class RoomPage extends StatefulWidget {
// //
@@ -25,7 +26,7 @@ class RoomPage extends StatefulWidget {
class _RoomPageState extends State<RoomPage> { class _RoomPageState extends State<RoomPage> {
// //
List<Participant> participants = []; List<ParticipantTrack> participantTracks = [];
EventsListener<RoomEvent> get _listener => widget.listener; EventsListener<RoomEvent> get _listener => widget.listener;
bool get fastConnection => widget.room.engine.fastConnectOptions != null; bool get fastConnection => widget.room.engine.fastConnectOptions != null;
@override @override
@@ -57,6 +58,8 @@ class _RoomPageState extends State<RoomPage> {
WidgetsBindingCompatible.instance WidgetsBindingCompatible.instance
?.addPostFrameCallback((timeStamp) => Navigator.pop(context)); ?.addPostFrameCallback((timeStamp) => Navigator.pop(context));
}) })
..on<LocalTrackPublishedEvent>((_) => _sortParticipants())
..on<LocalTrackUnpublishedEvent>((_) => _sortParticipants())
..on<DataReceivedEvent>((event) { ..on<DataReceivedEvent>((event) {
String decoded = 'Failed to decode'; String decoded = 'Failed to decode';
try { try {
@@ -90,13 +93,30 @@ class _RoomPageState extends State<RoomPage> {
} }
void _sortParticipants() { void _sortParticipants() {
List<Participant> participants = []; List<ParticipantTrack> userMediaTracks = [];
participants.addAll(widget.room.participants.values); List<ParticipantTrack> 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 // sort speakers for the grid
participants.sort((a, b) { userMediaTracks.sort((a, b) {
// loudest speaker first // loudest speaker first
if (a.isSpeaking && b.isSpeaking) { if (a.participant.isSpeaking && b.participant.isSpeaking) {
if (a.audioLevel > b.audioLevel) { if (a.participant.audioLevel > b.participant.audioLevel) {
return -1; return -1;
} else { } else {
return 1; return 1;
@@ -104,33 +124,43 @@ class _RoomPageState extends State<RoomPage> {
} }
// last spoken at // last spoken at
final aSpokeAt = a.lastSpokeAt?.millisecondsSinceEpoch ?? 0; final aSpokeAt = a.participant.lastSpokeAt?.millisecondsSinceEpoch ?? 0;
final bSpokeAt = b.lastSpokeAt?.millisecondsSinceEpoch ?? 0; final bSpokeAt = b.participant.lastSpokeAt?.millisecondsSinceEpoch ?? 0;
if (aSpokeAt != bSpokeAt) { if (aSpokeAt != bSpokeAt) {
return aSpokeAt > bSpokeAt ? -1 : 1; return aSpokeAt > bSpokeAt ? -1 : 1;
} }
// video on // video on
if (a.hasVideo != b.hasVideo) { if (a.participant.hasVideo != b.participant.hasVideo) {
return a.hasVideo ? -1 : 1; return a.participant.hasVideo ? -1 : 1;
} }
// joinedAt // joinedAt
return a.joinedAt.millisecondsSinceEpoch - return a.participant.joinedAt.millisecondsSinceEpoch -
b.joinedAt.millisecondsSinceEpoch; b.participant.joinedAt.millisecondsSinceEpoch;
}); });
final localParticipant = widget.room.localParticipant; final localParticipantTracks = widget.room.localParticipant?.videoTracks;
if (localParticipant != null) { if (localParticipantTracks != null) {
if (participants.length > 1) { for (var t in localParticipantTracks) {
participants.insert(1, localParticipant); if (t.isScreenShare) {
} else { screenTracks.add(ParticipantTrack(
participants.add(localParticipant); participant: widget.room.localParticipant!,
videoTrack: t.track,
isScreenShare: true,
));
} else {
userMediaTracks.add(ParticipantTrack(
participant: widget.room.localParticipant!,
videoTrack: t.track,
isScreenShare: false,
));
}
} }
} }
setState(() { setState(() {
this.participants = participants; participantTracks = [...screenTracks, ...userMediaTracks];
}); });
} }
@@ -139,18 +169,19 @@ class _RoomPageState extends State<RoomPage> {
body: Column( body: Column(
children: [ children: [
Expanded( Expanded(
child: participants.isNotEmpty child: participantTracks.isNotEmpty
? ParticipantWidget.widgetFor(participants.first) ? ParticipantWidget.widgetFor(participantTracks.first)
: Container()), : Container()),
SizedBox( SizedBox(
height: 100, height: 100,
child: ListView.builder( child: ListView.builder(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
itemCount: math.max(0, participants.length - 1), itemCount: math.max(0, participantTracks.length - 1),
itemBuilder: (BuildContext context, int index) => SizedBox( itemBuilder: (BuildContext context, int index) => SizedBox(
width: 100, width: 100,
height: 100, height: 100,
child: ParticipantWidget.widgetFor(participants[index + 1]), child:
ParticipantWidget.widgetFor(participantTracks[index + 1]),
), ),
), ),
), ),
+26 -3
View File
@@ -6,6 +6,7 @@ import 'package:eva_icons_flutter/eva_icons_flutter.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_background/flutter_background.dart'; import 'package:flutter_background/flutter_background.dart';
import 'package:livekit_client/livekit_client.dart'; import 'package:livekit_client/livekit_client.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart';
import '../exts.dart'; import '../exts.dart';
@@ -86,9 +87,30 @@ class _ControlsWidgetState extends State<ControlsWidget> {
} }
void _enableScreenShare() async { void _enableScreenShare() async {
await participant.setScreenShareEnabled(true); if (WebRTC.platformIsDesktop) {
try {
if (Platform.isAndroid) { final source = await showDialog<DesktopCapturerSource>(
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 // Android specific
try { try {
// Required for android screenshare. // Required for android screenshare.
@@ -105,6 +127,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
print('could not publish video: $e'); print('could not publish video: $e');
} }
} }
await participant.setScreenShareEnabled(true);
} }
void _disableScreenShare() async { void _disableScreenShare() async {
+48 -39
View File
@@ -10,17 +10,25 @@ import 'participant_info.dart';
abstract class ParticipantWidget extends StatefulWidget { abstract class ParticipantWidget extends StatefulWidget {
// Convenience method to return relevant widget for participant // Convenience method to return relevant widget for participant
static ParticipantWidget widgetFor(Participant participant) { static ParticipantWidget widgetFor(ParticipantTrack participantTrack) {
if (participant is LocalParticipant) { if (participantTrack.participant is LocalParticipant) {
return LocalParticipantWidget(participant); return LocalParticipantWidget(
} else if (participant is RemoteParticipant) { participantTrack.participant as LocalParticipant,
return RemoteParticipantWidget(participant); 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'); throw UnimplementedError('Unknown participant type');
} }
// Must be implemented by child class // Must be implemented by child class
abstract final Participant participant; abstract final Participant participant;
abstract final VideoTrack? videoTrack;
abstract final bool isScreenShare;
final VideoQuality quality; final VideoQuality quality;
const ParticipantWidget({ const ParticipantWidget({
@@ -32,9 +40,15 @@ abstract class ParticipantWidget extends StatefulWidget {
class LocalParticipantWidget extends ParticipantWidget { class LocalParticipantWidget extends ParticipantWidget {
@override @override
final LocalParticipant participant; final LocalParticipant participant;
@override
final VideoTrack? videoTrack;
@override
final bool isScreenShare;
const LocalParticipantWidget( const LocalParticipantWidget(
this.participant, { this.participant,
this.videoTrack,
this.isScreenShare, {
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
@@ -45,9 +59,15 @@ class LocalParticipantWidget extends ParticipantWidget {
class RemoteParticipantWidget extends ParticipantWidget { class RemoteParticipantWidget extends ParticipantWidget {
@override @override
final RemoteParticipant participant; final RemoteParticipant participant;
@override
final VideoTrack? videoTrack;
@override
final bool isScreenShare;
const RemoteParticipantWidget( const RemoteParticipantWidget(
this.participant, { this.participant,
this.videoTrack,
this.isScreenShare, {
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
@@ -60,7 +80,7 @@ abstract class _ParticipantWidgetState<T extends ParticipantWidget>
// //
bool _visible = true; bool _visible = true;
VideoTrack? get activeVideoTrack; VideoTrack? get activeVideoTrack;
TrackPublication? get firstVideoPublication; TrackPublication? get videoPublication;
TrackPublication? get firstAudioPublication; TrackPublication? get firstAudioPublication;
@override @override
@@ -89,12 +109,12 @@ abstract class _ParticipantWidgetState<T extends ParticipantWidget>
void _onParticipantChanged() => setState(() {}); void _onParticipantChanged() => setState(() {});
// Widgets to show above the info bar // Widgets to show above the info bar
List<Widget> extraWidgets() => []; List<Widget> extraWidgets(bool isScreenShare) => [];
@override @override
Widget build(BuildContext ctx) => Container( Widget build(BuildContext ctx) => Container(
foregroundDecoration: BoxDecoration( foregroundDecoration: BoxDecoration(
border: widget.participant.isSpeaking border: widget.participant.isSpeaking && !widget.isScreenShare
? Border.all( ? Border.all(
width: 5, width: 5,
color: LKColors.lkBlue, color: LKColors.lkBlue,
@@ -112,7 +132,7 @@ abstract class _ParticipantWidgetState<T extends ParticipantWidget>
child: activeVideoTrack != null child: activeVideoTrack != null
? VideoTrackRenderer( ? VideoTrackRenderer(
activeVideoTrack!, activeVideoTrack!,
fit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover, fit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain,
) )
: const NoVideoWidget(), : const NoVideoWidget(),
), ),
@@ -124,7 +144,7 @@ abstract class _ParticipantWidgetState<T extends ParticipantWidget>
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
...extraWidgets(), ...extraWidgets(widget.isScreenShare),
ParticipantInfoWidget( ParticipantInfoWidget(
title: widget.participant.name.isNotEmpty title: widget.participant.name.isNotEmpty
? '${widget.participant.name} (${widget.participant.identity})' ? '${widget.participant.name} (${widget.participant.identity})'
@@ -132,6 +152,7 @@ abstract class _ParticipantWidgetState<T extends ParticipantWidget>
audioAvailable: firstAudioPublication?.muted == false && audioAvailable: firstAudioPublication?.muted == false &&
firstAudioPublication?.subscribed == true, firstAudioPublication?.subscribed == true,
connectionQuality: widget.participant.connectionQuality, connectionQuality: widget.participant.connectionQuality,
isScreenShare: widget.isScreenShare,
), ),
], ],
), ),
@@ -144,60 +165,48 @@ abstract class _ParticipantWidgetState<T extends ParticipantWidget>
class _LocalParticipantWidgetState class _LocalParticipantWidgetState
extends _ParticipantWidgetState<LocalParticipantWidget> { extends _ParticipantWidgetState<LocalParticipantWidget> {
@override @override
LocalTrackPublication<LocalVideoTrack>? get firstVideoPublication => LocalTrackPublication<LocalVideoTrack>? get videoPublication =>
widget.participant.videoTracks.firstOrNull; widget.participant.videoTracks
.where((element) => element.sid == widget.videoTrack?.sid)
.firstOrNull;
@override @override
LocalTrackPublication<LocalAudioTrack>? get firstAudioPublication => LocalTrackPublication<LocalAudioTrack>? get firstAudioPublication =>
widget.participant.audioTracks.firstOrNull; widget.participant.audioTracks.firstOrNull;
@override @override
VideoTrack? get activeVideoTrack { VideoTrack? get activeVideoTrack => widget.videoTrack;
if (firstVideoPublication?.subscribed == true &&
firstVideoPublication?.muted == false &&
_visible) {
return firstVideoPublication?.track;
}
return null;
}
} }
class _RemoteParticipantWidgetState class _RemoteParticipantWidgetState
extends _ParticipantWidgetState<RemoteParticipantWidget> { extends _ParticipantWidgetState<RemoteParticipantWidget> {
@override @override
RemoteTrackPublication<RemoteVideoTrack>? get firstVideoPublication => RemoteTrackPublication<RemoteVideoTrack>? get videoPublication =>
widget.participant.videoTracks.firstOrNull; widget.participant.videoTracks
.where((element) => element.sid == widget.videoTrack?.sid)
.firstOrNull;
@override @override
RemoteTrackPublication<RemoteAudioTrack>? get firstAudioPublication => RemoteTrackPublication<RemoteAudioTrack>? get firstAudioPublication =>
widget.participant.audioTracks.firstOrNull; widget.participant.audioTracks.firstOrNull;
@override @override
VideoTrack? get activeVideoTrack { VideoTrack? get activeVideoTrack => widget.videoTrack;
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;
}
@override @override
List<Widget> extraWidgets() => [ List<Widget> extraWidgets(bool isScreenShare) => [
Row( Row(
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
// Menu for RemoteTrackPublication<RemoteVideoTrack> // Menu for RemoteTrackPublication<RemoteVideoTrack>
if (firstVideoPublication != null) if (videoPublication != null)
RemoteTrackPublicationMenuWidget( RemoteTrackPublicationMenuWidget(
pub: firstVideoPublication!, pub: videoPublication!,
icon: EvaIcons.video, icon: isScreenShare ? EvaIcons.monitor : EvaIcons.video,
), ),
// Menu for RemoteTrackPublication<RemoteAudioTrack> // Menu for RemoteTrackPublication<RemoteAudioTrack>
if (firstAudioPublication != null) if (firstAudioPublication != null && !isScreenShare)
RemoteTrackPublicationMenuWidget( RemoteTrackPublicationMenuWidget(
pub: firstAudioPublication!, pub: firstAudioPublication!,
icon: EvaIcons.volumeUp, icon: EvaIcons.volumeUp,
+29 -8
View File
@@ -2,16 +2,28 @@ import 'package:eva_icons_flutter/eva_icons_flutter.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:livekit_client/livekit_client.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 { class ParticipantInfoWidget extends StatelessWidget {
// //
final String? title; final String? title;
final bool audioAvailable; final bool audioAvailable;
final ConnectionQuality connectionQuality; final ConnectionQuality connectionQuality;
final bool isScreenShare;
const ParticipantInfoWidget({ const ParticipantInfoWidget({
this.title, this.title,
this.audioAvailable = true, this.audioAvailable = true,
this.connectionQuality = ConnectionQuality.unknown, this.connectionQuality = ConnectionQuality.unknown,
this.isScreenShare = false,
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
@@ -33,14 +45,23 @@ class ParticipantInfoWidget extends StatelessWidget {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
), ),
Padding( isScreenShare
padding: const EdgeInsets.only(left: 5), ? const Padding(
child: Icon( padding: EdgeInsets.only(left: 5),
audioAvailable ? EvaIcons.mic : EvaIcons.micOff, child: Icon(
color: audioAvailable ? Colors.white : Colors.red, EvaIcons.monitor,
size: 16, 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) if (connectionQuality != ConnectionQuality.unknown)
Padding( Padding(
padding: const EdgeInsets.only(left: 5), padding: const EdgeInsets.only(left: 5),
+7 -7
View File
@@ -1,18 +1,18 @@
PODS: PODS:
- device_info_plus_macos (0.0.1): - device_info_plus_macos (0.0.1):
- FlutterMacOS - FlutterMacOS
- flutter_webrtc (0.7.1): - flutter_webrtc (0.9.0):
- FlutterMacOS - FlutterMacOS
- WebRTC-SDK (= 97.4692.06) - WebRTC-SDK (= 97.4692.07)
- FlutterMacOS (1.0.0) - FlutterMacOS (1.0.0)
- livekit_client (1.0.0): - livekit_client (1.0.0):
- FlutterMacOS - FlutterMacOS
- WebRTC-SDK (~> 97.4692) - WebRTC-SDK (~> 97.4692.07)
- path_provider_macos (0.0.1): - path_provider_macos (0.0.1):
- FlutterMacOS - FlutterMacOS
- shared_preferences_macos (0.0.1): - shared_preferences_macos (0.0.1):
- FlutterMacOS - FlutterMacOS
- WebRTC-SDK (97.4692.06) - WebRTC-SDK (97.4692.07)
DEPENDENCIES: DEPENDENCIES:
- device_info_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus_macos/macos`) - device_info_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus_macos/macos`)
@@ -42,12 +42,12 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS: SPEC CHECKSUMS:
device_info_plus_macos: 1ad388a1ef433505c4038e7dd9605aadd1e2e9c7 device_info_plus_macos: 1ad388a1ef433505c4038e7dd9605aadd1e2e9c7
flutter_webrtc: a019f551482678a0341c79deacbf8d14ab241ceb flutter_webrtc: 708996d57dcf06da1c6a8e820a43112618d62213
FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424 FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424
livekit_client: 7479527f38e9c4a4dc2d3c58f99ab0dd531d24d6 livekit_client: 758f6a39a322a64e2081253b89c66d6f5275816f
path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19 path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19
shared_preferences_macos: a64dc611287ed6cbe28fd1297898db1336975727 shared_preferences_macos: a64dc611287ed6cbe28fd1297898db1336975727
WebRTC-SDK: eca7a6620a3897801207803672b3d036fb40b9d5 WebRTC-SDK: e16ed5e9f54d2a983d5f1f479a4b55f20da202fe
PODFILE CHECKSUM: 6eac6b3292e5142cfc23bdeb71848a40ec51c14c PODFILE CHECKSUM: 6eac6b3292e5142cfc23bdeb71848a40ec51c14c
+1 -1
View File
@@ -176,7 +176,7 @@ packages:
name: flutter_webrtc name: flutter_webrtc
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.8.12" version: "0.9.0"
google_fonts: google_fonts:
dependency: "direct main" dependency: "direct main"
description: description:
+2 -1
View File
@@ -28,5 +28,6 @@ export 'src/types/participant_permissions.dart';
export 'src/types/video_dimensions.dart'; export 'src/types/video_dimensions.dart';
export 'src/types/video_encoding.dart'; export 'src/types/video_encoding.dart';
export 'src/types/video_parameters.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; export 'src/extensions.dart' show WidgetsBindingCompatible;
+45 -18
View File
@@ -1,6 +1,5 @@
import 'dart:io';
import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter_webrtc/flutter_webrtc.dart';
import '../track/local/audio.dart'; import '../track/local/audio.dart';
import '../track/local/video.dart'; import '../track/local/video.dart';
@@ -23,26 +22,22 @@ extension CameraPositionExt on CameraPosition {
/// Options used when creating a [LocalVideoTrack] that captures the camera. /// Options used when creating a [LocalVideoTrack] that captures the camera.
class CameraCaptureOptions extends VideoCaptureOptions { class CameraCaptureOptions extends VideoCaptureOptions {
/// The deviceId of the capture device to use.
/// Available deviceIds can be obtained through `flutter_webrtc`:
/// <pre>
/// import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
///
/// List<MediaDeviceInfo> devices = await rtc.navigator.mediaDevices.enumerateDevices();
/// </pre>
final String? deviceId;
final CameraPosition cameraPosition; final CameraPosition cameraPosition;
const CameraCaptureOptions({ const CameraCaptureOptions({
this.deviceId,
this.cameraPosition = CameraPosition.front, this.cameraPosition = CameraPosition.front,
String? deviceId,
double? maxFrameRate,
VideoParameters params = VideoParametersPresets.h540_169, VideoParameters params = VideoParametersPresets.h540_169,
}) : super(params: params); }) : super(params: params, deviceId: deviceId, maxFrameRate: maxFrameRate);
CameraCaptureOptions.from({required VideoCaptureOptions captureOptions}) CameraCaptureOptions.from({required VideoCaptureOptions captureOptions})
: deviceId = null, : cameraPosition = CameraPosition.front,
cameraPosition = CameraPosition.front, super(
super(params: captureOptions.params); params: captureOptions.params,
deviceId: captureOptions.deviceId,
maxFrameRate: captureOptions.maxFrameRate,
);
@override @override
Map<String, dynamic> toMediaConstraintsMap() { Map<String, dynamic> toMediaConstraintsMap() {
@@ -60,6 +55,9 @@ class CameraCaptureOptions extends VideoCaptureOptions {
]; ];
} }
} }
if (maxFrameRate != null) {
constraints['frameRate'] = {'max': maxFrameRate};
}
return constraints; return constraints;
} }
@@ -67,21 +65,26 @@ class CameraCaptureOptions extends VideoCaptureOptions {
CameraCaptureOptions copyWith({ CameraCaptureOptions copyWith({
VideoParameters? params, VideoParameters? params,
CameraPosition? cameraPosition, CameraPosition? cameraPosition,
String? deviceId,
double? maxFrameRate,
}) => }) =>
CameraCaptureOptions( CameraCaptureOptions(
params: params ?? this.params, params: params ?? this.params,
cameraPosition: cameraPosition ?? this.cameraPosition, cameraPosition: cameraPosition ?? this.cameraPosition,
deviceId: deviceId ?? this.deviceId,
maxFrameRate: maxFrameRate ?? this.maxFrameRate,
); );
} }
/// Options used when creating a [LocalVideoTrack] that captures the screen. /// Options used when creating a [LocalVideoTrack] that captures the screen.
class ScreenShareCaptureOptions extends VideoCaptureOptions { class ScreenShareCaptureOptions extends VideoCaptureOptions {
final bool useiOSBroadcastExtension; final bool useiOSBroadcastExtension;
const ScreenShareCaptureOptions({ const ScreenShareCaptureOptions({
this.useiOSBroadcastExtension = false, this.useiOSBroadcastExtension = false,
String? sourceId,
double? maxFrameRate,
VideoParameters params = VideoParametersPresets.screenShareH720FPS15, VideoParameters params = VideoParametersPresets.screenShareH720FPS15,
}) : super(params: params); }) : super(params: params, deviceId: sourceId, maxFrameRate: maxFrameRate);
ScreenShareCaptureOptions.from( ScreenShareCaptureOptions.from(
{this.useiOSBroadcastExtension = false, {this.useiOSBroadcastExtension = false,
@@ -91,9 +94,17 @@ class ScreenShareCaptureOptions extends VideoCaptureOptions {
@override @override
Map<String, dynamic> toMediaConstraintsMap() { Map<String, dynamic> toMediaConstraintsMap() {
var constraints = super.toMediaConstraintsMap(); var constraints = super.toMediaConstraintsMap();
if (useiOSBroadcastExtension && Platform.isIOS) { if (useiOSBroadcastExtension && WebRTC.platformIsIOS) {
constraints['deviceId'] = 'broadcast'; constraints['deviceId'] = 'broadcast';
} }
if (WebRTC.platformIsDesktop) {
if (deviceId != null) {
constraints['deviceId'] = {'exact': deviceId};
}
if (maxFrameRate != 0.0) {
constraints['mandatory'] = {'frameRate': maxFrameRate};
}
}
return constraints; return constraints;
} }
} }
@@ -111,8 +122,24 @@ abstract class VideoCaptureOptions extends LocalTrackOptions {
// final LocalVideoTrackType type; // final LocalVideoTrackType type;
final VideoParameters params; final VideoParameters params;
/// The deviceId of the capture device to use.
/// Available deviceIds can be obtained through `flutter_webrtc`:
/// <pre>
/// import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
///
/// List<MediaDeviceInfo> devices = await rtc.navigator.mediaDevices.enumerateDevices();
/// // or
/// List<DesktopCapturerSource> desktopSources = await rtc.desktopCapturer.getSources(types: [rtc.SourceType.Screen, rtc.SourceType.Window]);
/// </pre>
final String? deviceId;
// Limit the maximum frameRate of the capture device.
final double? maxFrameRate;
const VideoCaptureOptions({ const VideoCaptureOptions({
this.params = VideoParametersPresets.h540_169, this.params = VideoParametersPresets.h540_169,
this.deviceId,
this.maxFrameRate,
}); });
@override @override
+318
View File
@@ -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<String, rtc.DesktopCapturerSource> _sources = {};
rtc.SourceType _sourceType = rtc.SourceType.Screen;
rtc.DesktopCapturerSource? _selectedSource;
final List<StreamSubscription<rtc.DesktopCapturerSource>> _subscriptions = [];
StateSetter? _stateSetter;
Timer? _timer;
void _ok(BuildContext context) {
_timer?.cancel();
for (var item in _subscriptions) {
item.cancel();
}
Navigator.pop<rtc.DesktopCapturerSource>(context, _selectedSource);
}
void _cancel(BuildContext context) {
_timer?.cancel();
for (var item in _subscriptions) {
item.cancel();
}
Navigator.pop<rtc.DesktopCapturerSource>(context, null);
}
Future<void> _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: <Widget>[
Padding(
padding: const EdgeInsets.all(10),
child: Stack(
children: <Widget>[
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: <Widget>[
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: <Widget>[
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);
},
),
],
),
),
],
),
)),
);
}
}
+1 -1
View File
@@ -16,5 +16,5 @@ Pod::Spec.new do |s|
s.static_framework = true s.static_framework = true
s.dependency 'FlutterMacOS' s.dependency 'FlutterMacOS'
s.dependency 'WebRTC-SDK', '~> 97.4692' s.dependency 'WebRTC-SDK', '~> 97.4692.07'
end end
+1 -1
View File
@@ -218,7 +218,7 @@ packages:
name: flutter_webrtc name: flutter_webrtc
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.8.10" version: "0.9.0"
glob: glob:
dependency: transitive dependency: transitive
description: description:
+1 -1
View File
@@ -22,7 +22,7 @@ dependencies:
uuid: ^3.0.6 uuid: ^3.0.6
synchronized: ^3.0.0 synchronized: ^3.0.0
protobuf: ^2.0.1 protobuf: ^2.0.1
flutter_webrtc: ^0.8.12 flutter_webrtc: ^0.9.0
dart_webrtc: ^1.0.6 dart_webrtc: ^1.0.6
device_info_plus: ^3.2.3 device_info_plus: ^3.2.3