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:
Hiroshi Horie
2022-01-10 23:48:44 +07:00
committed by GitHub
parent 9a37129504
commit aef1fd8804
12 changed files with 259 additions and 24 deletions
+19
View File
@@ -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'),
),
],
),
);
}
+18
View File
@@ -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',
),
],
),
);
+22 -18
View File
@@ -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(),
),
],
),
);
}
+2
View File
@@ -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.');
+19
View File
@@ -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}');
+18
View File
@@ -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(),
));
+15
View File
@@ -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,
});
}
+8
View File
@@ -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,
);
}
+13
View File
@@ -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
// ----------------------------------------------------------------------
+27
View File
@@ -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(),
);
}
+73 -6
View File
@@ -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;
}
}
+25
View File
@@ -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,
);
}