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:
+54
-23
@@ -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]),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user