Files
client-sdk-flutter/lib/src/publication/remote.dart
T

333 lines
10 KiB
Dart

import 'dart:async';
import 'dart:math';
import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart';
import 'package:meta/meta.dart';
import '../core/signal_client.dart';
import '../events.dart';
import '../extensions.dart';
import '../logger.dart';
import '../options.dart';
import '../participant/remote.dart';
import '../proto/livekit_models.pb.dart' as lk_models;
import '../proto/livekit_rtc.pb.dart' as lk_rtc;
import '../track/local/local.dart';
import '../track/remote/remote.dart';
import '../track/remote/video.dart';
import '../types.dart';
import '../utils.dart';
import 'track_publication.dart';
/// Represents a track publication from a RemoteParticipant. Provides methods to
/// control if we should subscribe to the track, and its quality (for video).
class RemoteTrackPublication<T extends RemoteTrack>
extends TrackPublication<T> {
/// The [RemoteParticipant] this [RemoteTrackPublication] belongs to.
@override
final RemoteParticipant participant;
bool get enabled => _enabled;
bool _enabled = true;
lk_models.VideoQuality _videoQuality = lk_models.VideoQuality.HIGH;
lk_models.VideoQuality get videoQuality => _videoQuality;
/// The server may pause the track when they are bandwidth limitations and resume
/// when there is more capacity. This property will be updated when the track is
/// paused / resumed by the server. See [TrackStreamStateUpdatedEvent] for the
/// relevant event.
StreamState get streamState => _streamState;
StreamState _streamState = StreamState.paused;
// latest TrackInfo
bool _metadataMuted = false;
// allowed to subscribe
bool _subscriptionAllowed = true;
bool get subscriptionAllowed => _subscriptionAllowed;
@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;
}
@internal
Future<void> updateStreamState(StreamState streamState) async {
// return if no change
if (_streamState == streamState) return;
_streamState = streamState;
[
participant.events,
participant.room.events,
].emit(TrackStreamStateUpdatedEvent(
participant: participant,
publication: this,
streamState: streamState,
));
}
// used to report renderer visibility to the server
// and optimize
lk_rtc.UpdateTrackSettings? _lastSentTrackSettings;
Timer? _visibilityTimer;
Function(lk_rtc.UpdateTrackSettings)? _setPendingTrackSettingsUpdateRequest;
Function? _cancelPendingTrackSettingsUpdateRequest;
RemoteTrackPublication({
required this.participant,
required lk_models.TrackInfo info,
T? track,
}) : super(info: info) {
logger.fine('RemoteTrackPublication.init track: $track, info: $info');
// register dispose func
onDispose(() async {
_cancelPendingTrackSettingsUpdateRequest?.call();
_visibilityTimer?.cancel();
// this object is responsible for disposing track
await this.track?.dispose();
});
_setPendingTrackSettingsUpdateRequest = Utils.createDebounceFunc(
_sendPendingTrackSettingsUpdateRequest,
cancelFunc: (func) => _cancelPendingTrackSettingsUpdateRequest = func,
wait: const Duration(milliseconds: 1500),
);
updateTrack(track);
}
@internal
@override
void updateFromInfo(lk_models.TrackInfo info) {
logger.fine(
'RemoteTrackPublication.updateFromInfo sid: ${info.sid} muted: ${info.muted}');
super.updateFromInfo(info);
track?.updateMuted(info.muted);
_metadataMuted = info.muted;
}
void _computeVideoViewVisibility({
bool quick = false,
}) {
//
Size maxOfSizes(Size s1, Size s2) => Size(
max(s1.width, s2.width),
max(s1.height, s2.height),
);
final videoTrack = track as VideoTrack;
final settings = lk_rtc.UpdateTrackSettings(
trackSids: [sid],
disabled: true,
);
// filter visible build contexts
final viewSizes = videoTrack.viewKeys
.map((e) => e.currentContext)
.whereNotNull()
.map((e) => e.findRenderObject() as RenderBox?)
.whereNotNull()
.map((e) => e.size);
logger.finer(
'[Visibility] ${track?.sid} watching ${viewSizes.length} views...');
if (viewSizes.isNotEmpty) {
// compute largest size
final largestSize =
viewSizes.reduce((value, element) => maxOfSizes(value, element));
settings
..disabled = false
..width = largestSize.width.ceil()
..height = largestSize.height.ceil();
}
// Only send new settings to server if it changed
if (settings != _lastSentTrackSettings) {
_lastSentTrackSettings = settings;
logger.fine('[Visibility] Change detected, quick: $quick');
if (quick) {
_sendPendingTrackSettingsUpdateRequest(settings);
} else {
_setPendingTrackSettingsUpdateRequest?.call(settings);
}
}
}
void _sendPendingTrackSettingsUpdateRequest(
lk_rtc.UpdateTrackSettings settings) {
logger.fine('[Visibility] Sending... ${settings.toProto3Json()}');
participant.room.engine.signalClient.sendUpdateTrackSettings(settings);
}
@internal
@override
Future<bool> updateTrack(covariant T? newValue) async {
logger.fine('RemoteTrackPublication.updateTrack track: $newValue');
final didUpdate = await super.updateTrack(newValue);
if (didUpdate) {
// Stop current visibility timer (if exists)
_cancelPendingTrackSettingsUpdateRequest?.call();
_visibilityTimer?.cancel();
final roomOptions = participant.room.roomOptions ?? const RoomOptions();
if (roomOptions.adaptiveStream && newValue is RemoteVideoTrack) {
// Start monitoring visibility
_visibilityTimer = Timer.periodic(
const Duration(milliseconds: 300),
(_) => _computeVideoViewVisibility(),
);
newValue.onVideoViewBuild = (_) {
logger.fine('[Visibility] VideoView did build');
if (_lastSentTrackSettings?.disabled == true) {
// quick enable
_cancelPendingTrackSettingsUpdateRequest?.call();
_computeVideoViewVisibility(quick: true);
}
};
}
if (newValue != null) {
// if new Track has been set to this RemoteTrackPublication,
// update the Track's muted state from the latest info.
newValue.updateMuted(
_metadataMuted,
shouldNotify: false, // don't emit event since this is initial state
);
}
}
return didUpdate;
}
set videoQuality(lk_models.VideoQuality newValue) {
if (newValue == _videoQuality) return;
_videoQuality = newValue;
_sendUpdateTrackSettings();
}
Future<void> enable() async {
if (_enabled) return;
_enabled = true;
_sendUpdateTrackSettings();
}
Future<void> disable() async {
if (!_enabled) return;
_enabled = false;
_sendUpdateTrackSettings();
}
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.
[participant.events, participant.room.events].emit(TrackUnsubscribedEvent(
participant: participant,
track: track!,
publication: this,
));
// Simply set to null for now
await updateTrack(null);
}
}
void _sendUpdateSubscription({required bool subscribed}) {
logger.fine('Sending update subscription... ${sid} ${subscribed}');
final subscription = lk_rtc.UpdateSubscription(
trackSids: [sid],
subscribe: subscribed,
);
participant.room.engine.signalClient.sendUpdateSubscription(subscription);
}
void _sendUpdateTrackSettings() {
final settings = lk_rtc.UpdateTrackSettings(
trackSids: [sid],
disabled: !_enabled,
);
if (kind == lk_models.TrackType.VIDEO) {
settings.quality = _videoQuality;
}
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,
publication: 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;
}
// Deprecated --------------------------------------------------
@Deprecated('use subscribe() or unsubscribe() instead')
set subscribed(bool newValue) {
logger.fine('Setting subscribed = ${newValue}');
newValue ? subscribe() : unsubscribe();
}
@Deprecated('Use enable() or disable() instead')
set enabled(bool newValue) {
logger.fine('Setting enabled = ${newValue}');
newValue ? enable() : disable();
}
}