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 {
|
void _onTapSendData() async {
|
||||||
final result = await context.showSendDataDialog();
|
final result = await context.showSendDataDialog();
|
||||||
if (result == true) {
|
if (result == true) {
|
||||||
@@ -220,6 +233,11 @@ class _ControlsWidgetState extends State<ControlsWidget> {
|
|||||||
icon: const Icon(EvaIcons.refresh),
|
icon: const Icon(EvaIcons.refresh),
|
||||||
tooltip: 're-connect',
|
tooltip: 're-connect',
|
||||||
),
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: _onTapUpdateSubscribePermission,
|
||||||
|
icon: const Icon(EvaIcons.settings2),
|
||||||
|
tooltip: 'Subscribe permission',
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -192,13 +192,13 @@ class _RemoteParticipantWidgetState
|
|||||||
if (firstVideoPublication != null)
|
if (firstVideoPublication != null)
|
||||||
RemoteTrackPublicationMenuWidget(
|
RemoteTrackPublicationMenuWidget(
|
||||||
pub: firstVideoPublication!,
|
pub: firstVideoPublication!,
|
||||||
icon: EvaIcons.videoOutline,
|
icon: EvaIcons.video,
|
||||||
),
|
),
|
||||||
// Menu for RemoteTrackPublication<RemoteAudioTrack>
|
// Menu for RemoteTrackPublication<RemoteAudioTrack>
|
||||||
if (firstAudioPublication != null)
|
if (firstAudioPublication != null)
|
||||||
RemoteTrackPublicationMenuWidget(
|
RemoteTrackPublicationMenuWidget(
|
||||||
pub: firstAudioPublication!,
|
pub: firstAudioPublication!,
|
||||||
icon: EvaIcons.volumeUpOutline,
|
icon: EvaIcons.volumeUp,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -218,23 +218,27 @@ class RemoteTrackPublicationMenuWidget extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) => Material(
|
Widget build(BuildContext context) => Material(
|
||||||
color: Colors.black.withOpacity(0.3),
|
color: Colors.black.withOpacity(0.3),
|
||||||
child: PopupMenuButton<Function>(
|
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(),
|
onSelected: (value) => value(),
|
||||||
itemBuilder: (BuildContext context) {
|
itemBuilder: (BuildContext context) => <PopupMenuEntry<Function>>[
|
||||||
return <PopupMenuEntry<Function>>[
|
// Subscribe/Unsubscribe
|
||||||
// Subscribe/Unsubscribe
|
if (pub.subscribed == false)
|
||||||
if (pub.subscribed == false)
|
PopupMenuItem(
|
||||||
PopupMenuItem(
|
child: const Text('Subscribe'),
|
||||||
child: const Text('Subscribe'),
|
value: () => pub.subscribe(),
|
||||||
value: () => pub.subscribed = true,
|
)
|
||||||
)
|
else if (pub.subscribed == true)
|
||||||
else if (pub.subscribed == true)
|
PopupMenuItem(
|
||||||
PopupMenuItem(
|
child: const Text('Un-subscribe'),
|
||||||
child: const Text('Un-subscribe'),
|
value: () => pub.unsubscribe(),
|
||||||
value: () => pub.subscribed = false,
|
),
|
||||||
),
|
],
|
||||||
];
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -621,6 +621,8 @@ class Engine extends Disposable with EventsEmittable<EngineEvent> {
|
|||||||
..on<SignalStreamStateUpdatedEvent>((event) => events.emit(event))
|
..on<SignalStreamStateUpdatedEvent>((event) => events.emit(event))
|
||||||
// relay to Room
|
// relay to Room
|
||||||
..on<SignalSubscribedQualityUpdatedEvent>((event) => events.emit(event))
|
..on<SignalSubscribedQualityUpdatedEvent>((event) => events.emit(event))
|
||||||
|
// relay to Room
|
||||||
|
..on<SignalSubscriptionPermissionUpdateEvent>((event) => events.emit(event))
|
||||||
..on<SignalLeaveEvent>((event) async {
|
..on<SignalLeaveEvent>((event) async {
|
||||||
if (connectionState == ConnectionState.reconnecting) {
|
if (connectionState == ConnectionState.reconnecting) {
|
||||||
logger.warning('Received leave signal while engine is 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);
|
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 {
|
..on<EngineTrackAddedEvent>((event) async {
|
||||||
logger.fine('EngineTrackAddedEvent trackSid:${event.track.id}');
|
logger.fine('EngineTrackAddedEvent trackSid:${event.track.id}');
|
||||||
|
|
||||||
|
|||||||
@@ -186,6 +186,13 @@ class SignalClient extends Disposable with EventsEmittable<SignalEvent> {
|
|||||||
updates: msg.subscribedQualityUpdate.subscribedQualities,
|
updates: msg.subscribedQualityUpdate.subscribedQualities,
|
||||||
));
|
));
|
||||||
break;
|
break;
|
||||||
|
case lk_rtc.SignalResponse_Message.subscriptionPermissionUpdate:
|
||||||
|
events.emit(SignalSubscriptionPermissionUpdateEvent(
|
||||||
|
participantSid: msg.subscriptionPermissionUpdate.participantSid,
|
||||||
|
trackSid: msg.subscriptionPermissionUpdate.trackSid,
|
||||||
|
allowed: msg.subscriptionPermissionUpdate.allowed,
|
||||||
|
));
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
logger.warning('skipping unsupported signal message');
|
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(
|
void sendLeave() => _sendRequest(lk_rtc.SignalRequest(
|
||||||
leave: lk_rtc.LeaveRequest(),
|
leave: lk_rtc.LeaveRequest(),
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -244,3 +244,18 @@ class SpeakingChangedEvent with ParticipantEvent {
|
|||||||
required this.speaking,
|
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',
|
lk_models.VideoQuality.LOW: 'q',
|
||||||
}[this]!;
|
}[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
|
// Engine events
|
||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
|
|||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
import '../core/room.dart';
|
import '../core/room.dart';
|
||||||
|
import '../core/signal_client.dart';
|
||||||
import '../events.dart';
|
import '../events.dart';
|
||||||
import '../exceptions.dart';
|
import '../exceptions.dart';
|
||||||
import '../extensions.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
|
// latest TrackInfo
|
||||||
bool _metadataMuted = false;
|
bool _metadataMuted = false;
|
||||||
|
|
||||||
|
// allowed to subscribe
|
||||||
|
bool _subscriptionAllowed = true;
|
||||||
|
bool get subscriptionAllowed => _subscriptionAllowed;
|
||||||
|
|
||||||
@internal
|
@internal
|
||||||
Future<void> updateStreamState(StreamState streamState) async {
|
Future<void> updateStreamState(StreamState streamState) async {
|
||||||
// return if no change
|
// return if no change
|
||||||
@@ -208,11 +212,27 @@ class RemoteTrackPublication<T extends RemoteTrack>
|
|||||||
_sendUpdateTrackSettings();
|
_sendUpdateTrackSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
set subscribed(bool newValue) {
|
@Deprecated('use subscribe() or unsubscribe() instead')
|
||||||
logger.fine('setting subscribed = ${newValue}');
|
set subscribed(bool val) {
|
||||||
if (newValue == super.subscribed) return;
|
logger.fine('setting subscribed = ${val}');
|
||||||
_sendUpdateSubscription(subscribed: newValue);
|
val ? subscribe() : unsubscribe();
|
||||||
if (!newValue && track != null) {
|
}
|
||||||
|
|
||||||
|
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
|
// Ideally, we should wait for WebRTC's onRemoveTrack event
|
||||||
// but it does not work reliably across platforms.
|
// but it does not work reliably across platforms.
|
||||||
// So for now we will assume remove track succeeded.
|
// So for now we will assume remove track succeeded.
|
||||||
@@ -222,7 +242,7 @@ class RemoteTrackPublication<T extends RemoteTrack>
|
|||||||
publication: this,
|
publication: this,
|
||||||
));
|
));
|
||||||
// Simply set to null for now
|
// 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);
|
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,
|
screenShareAudio,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum TrackSubscriptionState {
|
||||||
|
unsubscribed,
|
||||||
|
subscribed,
|
||||||
|
notAllowed,
|
||||||
|
}
|
||||||
|
|
||||||
/// The state of track data stream.
|
/// The state of track data stream.
|
||||||
/// This is controlled by server to optimize bandwidth.
|
/// This is controlled by server to optimize bandwidth.
|
||||||
enum StreamState {
|
enum StreamState {
|
||||||
@@ -159,3 +165,22 @@ class VideoDimensions {
|
|||||||
height ?? this.height,
|
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