Subscription permission API (#68)
* signal * request * implement * rename event * ref * adjustments based on review * override subscribed * move events emit * fix subscription state * minor change * update example for permissions demo * format & subscriptionState as computed property * prepare macos build * unsubscribed event when subscription not allowed
This commit is contained in:
@@ -135,4 +135,23 @@ extension LKExampleExt on BuildContext {
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
Future<bool?> showSubscribePermissionDialog() => showDialog<bool>(
|
||||
context: this,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('Allow subscription'),
|
||||
content: const Text(
|
||||
'Allow all participants to subscribe tracks published by local participant?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: const Text('NO'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
child: const Text('YES'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -136,6 +136,19 @@ class _ControlsWidgetState extends State<ControlsWidget> {
|
||||
}
|
||||
}
|
||||
|
||||
void _onTapUpdateSubscribePermission() async {
|
||||
final result = await context.showSubscribePermissionDialog();
|
||||
if (result != null) {
|
||||
try {
|
||||
widget.room.localParticipant?.setTrackSubscriptionPermissions(
|
||||
allParticipantsAllowed: result,
|
||||
);
|
||||
} catch (error) {
|
||||
await context.showErrorDialog(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _onTapSendData() async {
|
||||
final result = await context.showSendDataDialog();
|
||||
if (result == true) {
|
||||
@@ -220,6 +233,11 @@ class _ControlsWidgetState extends State<ControlsWidget> {
|
||||
icon: const Icon(EvaIcons.refresh),
|
||||
tooltip: 're-connect',
|
||||
),
|
||||
IconButton(
|
||||
onPressed: _onTapUpdateSubscribePermission,
|
||||
icon: const Icon(EvaIcons.settings2),
|
||||
tooltip: 'Subscribe permission',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -192,13 +192,13 @@ class _RemoteParticipantWidgetState
|
||||
if (firstVideoPublication != null)
|
||||
RemoteTrackPublicationMenuWidget(
|
||||
pub: firstVideoPublication!,
|
||||
icon: EvaIcons.videoOutline,
|
||||
icon: EvaIcons.video,
|
||||
),
|
||||
// Menu for RemoteTrackPublication<RemoteAudioTrack>
|
||||
if (firstAudioPublication != null)
|
||||
RemoteTrackPublicationMenuWidget(
|
||||
pub: firstAudioPublication!,
|
||||
icon: EvaIcons.volumeUpOutline,
|
||||
icon: EvaIcons.volumeUp,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -218,23 +218,27 @@ class RemoteTrackPublicationMenuWidget extends StatelessWidget {
|
||||
Widget build(BuildContext context) => Material(
|
||||
color: Colors.black.withOpacity(0.3),
|
||||
child: PopupMenuButton<Function>(
|
||||
icon: Icon(icon),
|
||||
tooltip: 'Subscribe menu',
|
||||
icon: Icon(icon,
|
||||
color: {
|
||||
TrackSubscriptionState.notAllowed: Colors.red,
|
||||
TrackSubscriptionState.unsubscribed: Colors.grey,
|
||||
TrackSubscriptionState.subscribed: Colors.green,
|
||||
}[pub.subscriptionState]),
|
||||
onSelected: (value) => value(),
|
||||
itemBuilder: (BuildContext context) {
|
||||
return <PopupMenuEntry<Function>>[
|
||||
// Subscribe/Unsubscribe
|
||||
if (pub.subscribed == false)
|
||||
PopupMenuItem(
|
||||
child: const Text('Subscribe'),
|
||||
value: () => pub.subscribed = true,
|
||||
)
|
||||
else if (pub.subscribed == true)
|
||||
PopupMenuItem(
|
||||
child: const Text('Un-subscribe'),
|
||||
value: () => pub.subscribed = false,
|
||||
),
|
||||
];
|
||||
},
|
||||
itemBuilder: (BuildContext context) => <PopupMenuEntry<Function>>[
|
||||
// Subscribe/Unsubscribe
|
||||
if (pub.subscribed == false)
|
||||
PopupMenuItem(
|
||||
child: const Text('Subscribe'),
|
||||
value: () => pub.subscribe(),
|
||||
)
|
||||
else if (pub.subscribed == true)
|
||||
PopupMenuItem(
|
||||
child: const Text('Un-subscribe'),
|
||||
value: () => pub.unsubscribe(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -621,6 +621,8 @@ class Engine extends Disposable with EventsEmittable<EngineEvent> {
|
||||
..on<SignalStreamStateUpdatedEvent>((event) => events.emit(event))
|
||||
// relay to Room
|
||||
..on<SignalSubscribedQualityUpdatedEvent>((event) => events.emit(event))
|
||||
// relay to Room
|
||||
..on<SignalSubscriptionPermissionUpdateEvent>((event) => events.emit(event))
|
||||
..on<SignalLeaveEvent>((event) async {
|
||||
if (connectionState == ConnectionState.reconnecting) {
|
||||
logger.warning('Received leave signal while engine is reconnecting.');
|
||||
|
||||
@@ -191,6 +191,25 @@ class Room extends DisposableChangeNotifier with EventsEmittable<RoomEvent> {
|
||||
}
|
||||
publication.updatePublishingLayers(event.updates);
|
||||
})
|
||||
..on<SignalSubscriptionPermissionUpdateEvent>((event) async {
|
||||
logger.fine('SignalSubscriptionPermissionUpdateEvent '
|
||||
'participantSid:${event.participantSid} '
|
||||
'trackSid:${event.trackSid} '
|
||||
'allowed:${event.allowed}');
|
||||
|
||||
// find participant
|
||||
final participant = _participants[event.participantSid];
|
||||
if (participant == null) {
|
||||
return;
|
||||
}
|
||||
// find track
|
||||
final publication = participant.trackPublications[event.trackSid];
|
||||
if (publication == null) {
|
||||
return;
|
||||
}
|
||||
//
|
||||
await publication.updateSubscriptionAllowed(event.allowed);
|
||||
})
|
||||
..on<EngineTrackAddedEvent>((event) async {
|
||||
logger.fine('EngineTrackAddedEvent trackSid:${event.track.id}');
|
||||
|
||||
|
||||
@@ -186,6 +186,13 @@ class SignalClient extends Disposable with EventsEmittable<SignalEvent> {
|
||||
updates: msg.subscribedQualityUpdate.subscribedQualities,
|
||||
));
|
||||
break;
|
||||
case lk_rtc.SignalResponse_Message.subscriptionPermissionUpdate:
|
||||
events.emit(SignalSubscriptionPermissionUpdateEvent(
|
||||
participantSid: msg.subscriptionPermissionUpdate.participantSid,
|
||||
trackSid: msg.subscriptionPermissionUpdate.trackSid,
|
||||
allowed: msg.subscriptionPermissionUpdate.allowed,
|
||||
));
|
||||
break;
|
||||
default:
|
||||
logger.warning('skipping unsupported signal message');
|
||||
}
|
||||
@@ -296,6 +303,17 @@ extension SignalClientRequests on SignalClient {
|
||||
),
|
||||
));
|
||||
|
||||
void sendUpdateSubscriptionPermissions({
|
||||
required bool allParticipants,
|
||||
required List<lk_rtc.TrackPermission> trackPermissions,
|
||||
}) =>
|
||||
_sendRequest(lk_rtc.SignalRequest(
|
||||
subscriptionPermissions: lk_rtc.UpdateSubscriptionPermissions(
|
||||
allParticipants: allParticipants,
|
||||
trackPermissions: trackPermissions,
|
||||
),
|
||||
));
|
||||
|
||||
void sendLeave() => _sendRequest(lk_rtc.SignalRequest(
|
||||
leave: lk_rtc.LeaveRequest(),
|
||||
));
|
||||
|
||||
@@ -244,3 +244,18 @@ class SpeakingChangedEvent with ParticipantEvent {
|
||||
required this.speaking,
|
||||
});
|
||||
}
|
||||
|
||||
/// One of subscribed tracks have changed its permissions for the current
|
||||
/// participant. If permission was revoked, then the track will no longer
|
||||
/// be subscribed. If permission was granted, a TrackSubscribed event will
|
||||
/// be emitted.
|
||||
class TrackSubscriptionPermissionChangedEvent with RoomEvent, ParticipantEvent {
|
||||
final Participant participant;
|
||||
final RemoteTrackPublication trackPublication;
|
||||
final TrackSubscriptionState state;
|
||||
const TrackSubscriptionPermissionChangedEvent({
|
||||
required this.participant,
|
||||
required this.trackPublication,
|
||||
required this.state,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -140,3 +140,11 @@ extension VideoQualityExt on lk_models.VideoQuality {
|
||||
lk_models.VideoQuality.LOW: 'q',
|
||||
}[this]!;
|
||||
}
|
||||
|
||||
extension ParticipantTrackPermissionExt on ParticipantTrackPermission {
|
||||
lk_rtc.TrackPermission toPBType() => lk_rtc.TrackPermission(
|
||||
participantSid: participantSid,
|
||||
allTracks: allTracksAllowed,
|
||||
trackSids: allowedTrackSids,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -207,6 +207,19 @@ class SignalSubscribedQualityUpdatedEvent
|
||||
});
|
||||
}
|
||||
|
||||
@internal
|
||||
class SignalSubscriptionPermissionUpdateEvent
|
||||
with SignalEvent, EngineEvent, InternalEvent {
|
||||
final String participantSid;
|
||||
final String trackSid;
|
||||
final bool allowed;
|
||||
const SignalSubscriptionPermissionUpdateEvent({
|
||||
required this.participantSid,
|
||||
required this.trackSid,
|
||||
required this.allowed,
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// Engine events
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
import '../core/room.dart';
|
||||
import '../core/signal_client.dart';
|
||||
import '../events.dart';
|
||||
import '../exceptions.dart';
|
||||
import '../extensions.dart';
|
||||
@@ -304,4 +305,30 @@ class LocalParticipant extends Participant<LocalTrackPublication> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Control who can subscribe to LocalParticipant's published tracks.
|
||||
///
|
||||
/// By default, all participants can subscribe. This allows fine-grained control over
|
||||
/// who is able to subscribe at a participant and track level.
|
||||
///
|
||||
/// Note: if access is given at a track-level (i.e. both [allParticipantsAllowed] and
|
||||
/// [ParticipantTrackPermission.allTracksAllowed] are false), any newer published tracks
|
||||
/// will not grant permissions to any participants and will require a subsequent
|
||||
/// permissions update to allow subscription.
|
||||
///
|
||||
/// [allParticipantsAllowed] Allows all participants to subscribe all tracks.
|
||||
/// Takes precedence over [trackPermissions] if set to true.
|
||||
/// By default this is set to true.
|
||||
///
|
||||
/// [trackPermissions] Full list of individual permissions per
|
||||
/// participant/track. Any omitted participants will not receive any permissions.
|
||||
|
||||
void setTrackSubscriptionPermissions({
|
||||
required bool allParticipantsAllowed,
|
||||
List<ParticipantTrackPermission> trackPermissions = const [],
|
||||
}) =>
|
||||
room.engine.signalClient.sendUpdateSubscriptionPermissions(
|
||||
allParticipants: allParticipantsAllowed,
|
||||
trackPermissions: trackPermissions.map((e) => e.toPBType()).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -42,6 +42,10 @@ class RemoteTrackPublication<T extends RemoteTrack>
|
||||
// latest TrackInfo
|
||||
bool _metadataMuted = false;
|
||||
|
||||
// allowed to subscribe
|
||||
bool _subscriptionAllowed = true;
|
||||
bool get subscriptionAllowed => _subscriptionAllowed;
|
||||
|
||||
@internal
|
||||
Future<void> updateStreamState(StreamState streamState) async {
|
||||
// return if no change
|
||||
@@ -208,11 +212,27 @@ class RemoteTrackPublication<T extends RemoteTrack>
|
||||
_sendUpdateTrackSettings();
|
||||
}
|
||||
|
||||
set subscribed(bool newValue) {
|
||||
logger.fine('setting subscribed = ${newValue}');
|
||||
if (newValue == super.subscribed) return;
|
||||
_sendUpdateSubscription(subscribed: newValue);
|
||||
if (!newValue && track != null) {
|
||||
@Deprecated('use subscribe() or unsubscribe() instead')
|
||||
set subscribed(bool val) {
|
||||
logger.fine('setting subscribed = ${val}');
|
||||
val ? subscribe() : unsubscribe();
|
||||
}
|
||||
|
||||
Future<void> subscribe() async {
|
||||
if (super.subscribed || !_subscriptionAllowed) {
|
||||
logger.fine('ignoring subscribe() request...');
|
||||
return;
|
||||
}
|
||||
_sendUpdateSubscription(subscribed: true);
|
||||
}
|
||||
|
||||
Future<void> unsubscribe() async {
|
||||
if (!super.subscribed || !_subscriptionAllowed) {
|
||||
logger.fine('ignoring unsubscribe() request...');
|
||||
return;
|
||||
}
|
||||
_sendUpdateSubscription(subscribed: false);
|
||||
if (track != null) {
|
||||
// Ideally, we should wait for WebRTC's onRemoveTrack event
|
||||
// but it does not work reliably across platforms.
|
||||
// So for now we will assume remove track succeeded.
|
||||
@@ -222,7 +242,7 @@ class RemoteTrackPublication<T extends RemoteTrack>
|
||||
publication: this,
|
||||
));
|
||||
// Simply set to null for now
|
||||
updateTrack(null);
|
||||
await updateTrack(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,4 +265,51 @@ class RemoteTrackPublication<T extends RemoteTrack>
|
||||
}
|
||||
participant.room.engine.signalClient.sendUpdateTrackSettings(settings);
|
||||
}
|
||||
|
||||
@internal
|
||||
// Update internal var and return true if changed
|
||||
Future<bool> updateSubscriptionAllowed(bool allowed) async {
|
||||
if (_subscriptionAllowed == allowed) return false;
|
||||
_subscriptionAllowed = allowed;
|
||||
|
||||
logger.fine('updateSubscriptionAllowed allowed: ${allowed}');
|
||||
// emit events
|
||||
[
|
||||
participant.events,
|
||||
participant.room.events,
|
||||
].emit(TrackSubscriptionPermissionChangedEvent(
|
||||
participant: participant,
|
||||
trackPublication: this,
|
||||
state: subscriptionState,
|
||||
));
|
||||
|
||||
if (!_subscriptionAllowed && super.subscribed /* track != null */) {
|
||||
// Ideally, we should wait for WebRTC's onRemoveTrack event
|
||||
// but it does not work reliably across platforms.
|
||||
// So for now we will assume remove track succeeded.
|
||||
[participant.events, participant.room.events].emit(TrackUnsubscribedEvent(
|
||||
participant: participant,
|
||||
track: track!,
|
||||
publication: this,
|
||||
));
|
||||
// Simply set to null for now
|
||||
await updateTrack(null);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
bool get subscribed {
|
||||
// always return false when subscription is not allowed
|
||||
if (!_subscriptionAllowed) return false;
|
||||
return super.subscribed;
|
||||
}
|
||||
|
||||
TrackSubscriptionState get subscriptionState {
|
||||
if (!_subscriptionAllowed) return TrackSubscriptionState.notAllowed;
|
||||
return super.subscribed
|
||||
? TrackSubscriptionState.subscribed
|
||||
: TrackSubscriptionState.unsubscribed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,12 @@ enum TrackSource {
|
||||
screenShareAudio,
|
||||
}
|
||||
|
||||
enum TrackSubscriptionState {
|
||||
unsubscribed,
|
||||
subscribed,
|
||||
notAllowed,
|
||||
}
|
||||
|
||||
/// The state of track data stream.
|
||||
/// This is controlled by server to optimize bandwidth.
|
||||
enum StreamState {
|
||||
@@ -159,3 +165,22 @@ class VideoDimensions {
|
||||
height ?? this.height,
|
||||
);
|
||||
}
|
||||
|
||||
@immutable
|
||||
class ParticipantTrackPermission {
|
||||
/// The participant id this permission applies to.
|
||||
final String participantSid;
|
||||
|
||||
/// If set to true, the target participant can subscribe to all tracks from the local participant.
|
||||
/// Takes precedence over [allowedTrackSids].
|
||||
final bool allTracksAllowed;
|
||||
|
||||
/// The list of track ids that the target participant can subscribe to.
|
||||
final List<String> allowedTrackSids;
|
||||
|
||||
const ParticipantTrackPermission(
|
||||
this.participantSid,
|
||||
this.allTracksAllowed,
|
||||
this.allowedTrackSids,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user