Skip to content

Commit a554d92

Browse files
authored
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.
1 parent c772271 commit a554d92

File tree

13 files changed

+533
-103
lines changed

13 files changed

+533
-103
lines changed

example/lib/pages/room.dart

Lines changed: 54 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'package:livekit_client/livekit_client.dart';
77
import '../exts.dart';
88
import '../widgets/controls.dart';
99
import '../widgets/participant.dart';
10+
import '../widgets/participant_info.dart';
1011

1112
class RoomPage extends StatefulWidget {
1213
//
@@ -25,7 +26,7 @@ class RoomPage extends StatefulWidget {
2526

2627
class _RoomPageState extends State<RoomPage> {
2728
//
28-
List<Participant> participants = [];
29+
List<ParticipantTrack> participantTracks = [];
2930
EventsListener<RoomEvent> get _listener => widget.listener;
3031
bool get fastConnection => widget.room.engine.fastConnectOptions != null;
3132
@override
@@ -57,6 +58,8 @@ class _RoomPageState extends State<RoomPage> {
5758
WidgetsBindingCompatible.instance
5859
?.addPostFrameCallback((timeStamp) => Navigator.pop(context));
5960
})
61+
..on<LocalTrackPublishedEvent>((_) => _sortParticipants())
62+
..on<LocalTrackUnpublishedEvent>((_) => _sortParticipants())
6063
..on<DataReceivedEvent>((event) {
6164
String decoded = 'Failed to decode';
6265
try {
@@ -90,47 +93,74 @@ class _RoomPageState extends State<RoomPage> {
9093
}
9194

9295
void _sortParticipants() {
93-
List<Participant> participants = [];
94-
participants.addAll(widget.room.participants.values);
96+
List<ParticipantTrack> userMediaTracks = [];
97+
List<ParticipantTrack> screenTracks = [];
98+
for (var participant in widget.room.participants.values) {
99+
for (var t in participant.videoTracks) {
100+
if (t.isScreenShare) {
101+
screenTracks.add(ParticipantTrack(
102+
participant: participant,
103+
videoTrack: t.track,
104+
isScreenShare: true,
105+
));
106+
} else {
107+
userMediaTracks.add(ParticipantTrack(
108+
participant: participant,
109+
videoTrack: t.track,
110+
isScreenShare: false,
111+
));
112+
}
113+
}
114+
}
95115
// sort speakers for the grid
96-
participants.sort((a, b) {
116+
userMediaTracks.sort((a, b) {
97117
// loudest speaker first
98-
if (a.isSpeaking && b.isSpeaking) {
99-
if (a.audioLevel > b.audioLevel) {
118+
if (a.participant.isSpeaking && b.participant.isSpeaking) {
119+
if (a.participant.audioLevel > b.participant.audioLevel) {
100120
return -1;
101121
} else {
102122
return 1;
103123
}
104124
}
105125

106126
// last spoken at
107-
final aSpokeAt = a.lastSpokeAt?.millisecondsSinceEpoch ?? 0;
108-
final bSpokeAt = b.lastSpokeAt?.millisecondsSinceEpoch ?? 0;
127+
final aSpokeAt = a.participant.lastSpokeAt?.millisecondsSinceEpoch ?? 0;
128+
final bSpokeAt = b.participant.lastSpokeAt?.millisecondsSinceEpoch ?? 0;
109129

110130
if (aSpokeAt != bSpokeAt) {
111131
return aSpokeAt > bSpokeAt ? -1 : 1;
112132
}
113133

114134
// video on
115-
if (a.hasVideo != b.hasVideo) {
116-
return a.hasVideo ? -1 : 1;
135+
if (a.participant.hasVideo != b.participant.hasVideo) {
136+
return a.participant.hasVideo ? -1 : 1;
117137
}
118138

119139
// joinedAt
120-
return a.joinedAt.millisecondsSinceEpoch -
121-
b.joinedAt.millisecondsSinceEpoch;
140+
return a.participant.joinedAt.millisecondsSinceEpoch -
141+
b.participant.joinedAt.millisecondsSinceEpoch;
122142
});
123143

124-
final localParticipant = widget.room.localParticipant;
125-
if (localParticipant != null) {
126-
if (participants.length > 1) {
127-
participants.insert(1, localParticipant);
128-
} else {
129-
participants.add(localParticipant);
144+
final localParticipantTracks = widget.room.localParticipant?.videoTracks;
145+
if (localParticipantTracks != null) {
146+
for (var t in localParticipantTracks) {
147+
if (t.isScreenShare) {
148+
screenTracks.add(ParticipantTrack(
149+
participant: widget.room.localParticipant!,
150+
videoTrack: t.track,
151+
isScreenShare: true,
152+
));
153+
} else {
154+
userMediaTracks.add(ParticipantTrack(
155+
participant: widget.room.localParticipant!,
156+
videoTrack: t.track,
157+
isScreenShare: false,
158+
));
159+
}
130160
}
131161
}
132162
setState(() {
133-
this.participants = participants;
163+
participantTracks = [...screenTracks, ...userMediaTracks];
134164
});
135165
}
136166

@@ -139,18 +169,19 @@ class _RoomPageState extends State<RoomPage> {
139169
body: Column(
140170
children: [
141171
Expanded(
142-
child: participants.isNotEmpty
143-
? ParticipantWidget.widgetFor(participants.first)
172+
child: participantTracks.isNotEmpty
173+
? ParticipantWidget.widgetFor(participantTracks.first)
144174
: Container()),
145175
SizedBox(
146176
height: 100,
147177
child: ListView.builder(
148178
scrollDirection: Axis.horizontal,
149-
itemCount: math.max(0, participants.length - 1),
179+
itemCount: math.max(0, participantTracks.length - 1),
150180
itemBuilder: (BuildContext context, int index) => SizedBox(
151181
width: 100,
152182
height: 100,
153-
child: ParticipantWidget.widgetFor(participants[index + 1]),
183+
child:
184+
ParticipantWidget.widgetFor(participantTracks[index + 1]),
154185
),
155186
),
156187
),

example/lib/widgets/controls.dart

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'package:eva_icons_flutter/eva_icons_flutter.dart';
66
import 'package:flutter/material.dart';
77
import 'package:flutter_background/flutter_background.dart';
88
import 'package:livekit_client/livekit_client.dart';
9+
import 'package:flutter_webrtc/flutter_webrtc.dart';
910

1011
import '../exts.dart';
1112

@@ -86,9 +87,30 @@ class _ControlsWidgetState extends State<ControlsWidget> {
8687
}
8788

8889
void _enableScreenShare() async {
89-
await participant.setScreenShareEnabled(true);
90-
91-
if (Platform.isAndroid) {
90+
if (WebRTC.platformIsDesktop) {
91+
try {
92+
final source = await showDialog<DesktopCapturerSource>(
93+
context: context,
94+
builder: (context) => ScreenSelectDialog(),
95+
);
96+
if (source == null) {
97+
print('cancelled screenshare');
98+
return;
99+
}
100+
print('DesktopCapturerSource: ${source.id}');
101+
var track = await LocalVideoTrack.createScreenShareTrack(
102+
ScreenShareCaptureOptions(
103+
sourceId: source.id,
104+
maxFrameRate: 15.0,
105+
),
106+
);
107+
await participant.publishVideoTrack(track);
108+
} catch (e) {
109+
print('could not publish video: $e');
110+
}
111+
return;
112+
}
113+
if (WebRTC.platformIsAndroid) {
92114
// Android specific
93115
try {
94116
// Required for android screenshare.
@@ -105,6 +127,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
105127
print('could not publish video: $e');
106128
}
107129
}
130+
await participant.setScreenShareEnabled(true);
108131
}
109132

110133
void _disableScreenShare() async {

example/lib/widgets/participant.dart

Lines changed: 48 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,25 @@ import 'participant_info.dart';
1010

1111
abstract class ParticipantWidget extends StatefulWidget {
1212
// Convenience method to return relevant widget for participant
13-
static ParticipantWidget widgetFor(Participant participant) {
14-
if (participant is LocalParticipant) {
15-
return LocalParticipantWidget(participant);
16-
} else if (participant is RemoteParticipant) {
17-
return RemoteParticipantWidget(participant);
13+
static ParticipantWidget widgetFor(ParticipantTrack participantTrack) {
14+
if (participantTrack.participant is LocalParticipant) {
15+
return LocalParticipantWidget(
16+
participantTrack.participant as LocalParticipant,
17+
participantTrack.videoTrack,
18+
participantTrack.isScreenShare);
19+
} else if (participantTrack.participant is RemoteParticipant) {
20+
return RemoteParticipantWidget(
21+
participantTrack.participant as RemoteParticipant,
22+
participantTrack.videoTrack,
23+
participantTrack.isScreenShare);
1824
}
1925
throw UnimplementedError('Unknown participant type');
2026
}
2127

2228
// Must be implemented by child class
2329
abstract final Participant participant;
30+
abstract final VideoTrack? videoTrack;
31+
abstract final bool isScreenShare;
2432
final VideoQuality quality;
2533

2634
const ParticipantWidget({
@@ -32,9 +40,15 @@ abstract class ParticipantWidget extends StatefulWidget {
3240
class LocalParticipantWidget extends ParticipantWidget {
3341
@override
3442
final LocalParticipant participant;
43+
@override
44+
final VideoTrack? videoTrack;
45+
@override
46+
final bool isScreenShare;
3547

3648
const LocalParticipantWidget(
37-
this.participant, {
49+
this.participant,
50+
this.videoTrack,
51+
this.isScreenShare, {
3852
Key? key,
3953
}) : super(key: key);
4054

@@ -45,9 +59,15 @@ class LocalParticipantWidget extends ParticipantWidget {
4559
class RemoteParticipantWidget extends ParticipantWidget {
4660
@override
4761
final RemoteParticipant participant;
62+
@override
63+
final VideoTrack? videoTrack;
64+
@override
65+
final bool isScreenShare;
4866

4967
const RemoteParticipantWidget(
50-
this.participant, {
68+
this.participant,
69+
this.videoTrack,
70+
this.isScreenShare, {
5171
Key? key,
5272
}) : super(key: key);
5373

@@ -60,7 +80,7 @@ abstract class _ParticipantWidgetState<T extends ParticipantWidget>
6080
//
6181
bool _visible = true;
6282
VideoTrack? get activeVideoTrack;
63-
TrackPublication? get firstVideoPublication;
83+
TrackPublication? get videoPublication;
6484
TrackPublication? get firstAudioPublication;
6585

6686
@override
@@ -89,12 +109,12 @@ abstract class _ParticipantWidgetState<T extends ParticipantWidget>
89109
void _onParticipantChanged() => setState(() {});
90110

91111
// Widgets to show above the info bar
92-
List<Widget> extraWidgets() => [];
112+
List<Widget> extraWidgets(bool isScreenShare) => [];
93113

94114
@override
95115
Widget build(BuildContext ctx) => Container(
96116
foregroundDecoration: BoxDecoration(
97-
border: widget.participant.isSpeaking
117+
border: widget.participant.isSpeaking && !widget.isScreenShare
98118
? Border.all(
99119
width: 5,
100120
color: LKColors.lkBlue,
@@ -112,7 +132,7 @@ abstract class _ParticipantWidgetState<T extends ParticipantWidget>
112132
child: activeVideoTrack != null
113133
? VideoTrackRenderer(
114134
activeVideoTrack!,
115-
fit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover,
135+
fit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain,
116136
)
117137
: const NoVideoWidget(),
118138
),
@@ -124,14 +144,15 @@ abstract class _ParticipantWidgetState<T extends ParticipantWidget>
124144
crossAxisAlignment: CrossAxisAlignment.stretch,
125145
mainAxisSize: MainAxisSize.min,
126146
children: [
127-
...extraWidgets(),
147+
...extraWidgets(widget.isScreenShare),
128148
ParticipantInfoWidget(
129149
title: widget.participant.name.isNotEmpty
130150
? '${widget.participant.name} (${widget.participant.identity})'
131151
: widget.participant.identity,
132152
audioAvailable: firstAudioPublication?.muted == false &&
133153
firstAudioPublication?.subscribed == true,
134154
connectionQuality: widget.participant.connectionQuality,
155+
isScreenShare: widget.isScreenShare,
135156
),
136157
],
137158
),
@@ -144,60 +165,48 @@ abstract class _ParticipantWidgetState<T extends ParticipantWidget>
144165
class _LocalParticipantWidgetState
145166
extends _ParticipantWidgetState<LocalParticipantWidget> {
146167
@override
147-
LocalTrackPublication<LocalVideoTrack>? get firstVideoPublication =>
148-
widget.participant.videoTracks.firstOrNull;
168+
LocalTrackPublication<LocalVideoTrack>? get videoPublication =>
169+
widget.participant.videoTracks
170+
.where((element) => element.sid == widget.videoTrack?.sid)
171+
.firstOrNull;
149172

150173
@override
151174
LocalTrackPublication<LocalAudioTrack>? get firstAudioPublication =>
152175
widget.participant.audioTracks.firstOrNull;
153176

154177
@override
155-
VideoTrack? get activeVideoTrack {
156-
if (firstVideoPublication?.subscribed == true &&
157-
firstVideoPublication?.muted == false &&
158-
_visible) {
159-
return firstVideoPublication?.track;
160-
}
161-
return null;
162-
}
178+
VideoTrack? get activeVideoTrack => widget.videoTrack;
163179
}
164180

165181
class _RemoteParticipantWidgetState
166182
extends _ParticipantWidgetState<RemoteParticipantWidget> {
167183
@override
168-
RemoteTrackPublication<RemoteVideoTrack>? get firstVideoPublication =>
169-
widget.participant.videoTracks.firstOrNull;
184+
RemoteTrackPublication<RemoteVideoTrack>? get videoPublication =>
185+
widget.participant.videoTracks
186+
.where((element) => element.sid == widget.videoTrack?.sid)
187+
.firstOrNull;
170188

171189
@override
172190
RemoteTrackPublication<RemoteAudioTrack>? get firstAudioPublication =>
173191
widget.participant.audioTracks.firstOrNull;
174192

175193
@override
176-
VideoTrack? get activeVideoTrack {
177-
for (final trackPublication in widget.participant.videoTracks) {
178-
print(
179-
'video track ${trackPublication.sid} subscribed ${trackPublication.subscribed} muted ${trackPublication.muted}');
180-
if (trackPublication.subscribed && !trackPublication.muted && _visible) {
181-
return trackPublication.track;
182-
}
183-
}
184-
return null;
185-
}
194+
VideoTrack? get activeVideoTrack => widget.videoTrack;
186195

187196
@override
188-
List<Widget> extraWidgets() => [
197+
List<Widget> extraWidgets(bool isScreenShare) => [
189198
Row(
190199
mainAxisSize: MainAxisSize.max,
191200
mainAxisAlignment: MainAxisAlignment.end,
192201
children: [
193202
// Menu for RemoteTrackPublication<RemoteVideoTrack>
194-
if (firstVideoPublication != null)
203+
if (videoPublication != null)
195204
RemoteTrackPublicationMenuWidget(
196-
pub: firstVideoPublication!,
197-
icon: EvaIcons.video,
205+
pub: videoPublication!,
206+
icon: isScreenShare ? EvaIcons.monitor : EvaIcons.video,
198207
),
199208
// Menu for RemoteTrackPublication<RemoteAudioTrack>
200-
if (firstAudioPublication != null)
209+
if (firstAudioPublication != null && !isScreenShare)
201210
RemoteTrackPublicationMenuWidget(
202211
pub: firstAudioPublication!,
203212
icon: EvaIcons.volumeUp,

0 commit comments

Comments
 (0)