diff --git a/lib/src/publication/remote.dart b/lib/src/publication/remote.dart index f9e30bb5e..be82dcef1 100644 --- a/lib/src/publication/remote.dart +++ b/lib/src/publication/remote.dart @@ -97,7 +97,6 @@ class RemoteTrackPublication extends TrackPublication // used to report renderer visibility to the server // and optimize lk_rtc.UpdateTrackSettings? _lastSentTrackSettings; - Timer? _visibilityTimer; Function(lk_rtc.UpdateTrackSettings)? _setPendingTrackSettingsUpdateRequest; Function? _cancelPendingTrackSettingsUpdateRequest; @@ -112,7 +111,6 @@ class RemoteTrackPublication extends TrackPublication // register dispose func onDispose(() async { _cancelPendingTrackSettingsUpdateRequest?.call(); - _visibilityTimer?.cancel(); // this object is responsible for disposing track await this.track?.dispose(); }); @@ -133,41 +131,44 @@ class RemoteTrackPublication extends TrackPublication _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), - ); + Size _maxOfSizes(Size s1, Size s2) => Size( + max(s1.width, s2.width), + max(s1.height, s2.height), + ); - final videoTrack = track as VideoTrack; + /// Requests the server to send a new track with the given settings. + @internal + void updateVideoViewSize(String viewViewId, Size size, {bool quick = false}) { + assert(track is VideoTrack, 'updateVideoViewSize can only be called on video tracks'); + final videoTrack = track as VideoTrack?; + if (videoTrack == null) return; + if (videoTrack.viewSizes[viewViewId] == null) { + logger.warning( + 'Trying to update video view size ${size} for a view ${viewViewId} that is not registered or was unregistered', + ); + return; + } + if (videoTrack.viewSizes[viewViewId] == size) return; - final settings = lk_rtc.UpdateTrackSettings( - trackSids: [sid], - disabled: true, - ); + print('[Visibility] VideoView did resize to ${size.width}x${size.height}, quick: ${quick}'); + videoTrack.viewSizes[viewViewId] = size; + + final settings = lk_rtc.UpdateTrackSettings(trackSids: [sid], disabled: true); - // filter visible build contexts - final viewSizes = videoTrack.viewKeys - .map((e) => e.currentContext) - .nonNulls - .map((e) => e.findRenderObject() as RenderBox?) - .nonNulls - .where((e) => e.hasSize) - .map((e) => e.size); + final videoViewsSizes = [ + for (final MapEntry(key: _, value: size) in videoTrack.viewSizes.entries) + if (size > Size.zero) size, + ]; - logger.finer('[Visibility] ${track?.sid} watching ${viewSizes.length} views...'); + logger.finer('[Visibility] ${track?.sid} watching ${videoViewsSizes.length} views...'); - if (viewSizes.isNotEmpty) { - // compute largest size - final largestSize = viewSizes.reduce((value, element) => maxOfSizes(value, element)); + if (videoViewsSizes.isNotEmpty) { + final largestVideoView = videoViewsSizes.reduce((value, element) => _maxOfSizes(value, element)); settings ..disabled = false - ..width = largestSize.width.ceil() - ..height = largestSize.height.ceil(); + ..width = largestVideoView.width.round() + ..height = largestVideoView.height.round(); } // Only send new settings to server if it changed @@ -196,24 +197,21 @@ class RemoteTrackPublication extends TrackPublication if (didUpdate) { // Stop current visibility timer (if exists) _cancelPendingTrackSettingsUpdateRequest?.call(); - _visibilityTimer?.cancel(); final roomOptions = participant.room.roomOptions; if (roomOptions.adaptiveStream && newValue is RemoteVideoTrack) { - // Start monitoring visibility - _visibilityTimer = Timer.periodic( - const Duration(milliseconds: 300), - (_) => _computeVideoViewVisibility(), - ); - - newValue.onVideoViewBuild = (_) { + newValue.onVideoViewBuild = (viewId, size) { logger.finer('[Visibility] VideoView did build'); if (_lastSentTrackSettings?.disabled == true) { // quick enable _cancelPendingTrackSettingsUpdateRequest?.call(); - _computeVideoViewVisibility(quick: true); + updateVideoViewSize(viewId, size, quick: true); } }; + newValue.onViewViewResize = (viewId, size) { + // schedule update to server + updateVideoViewSize(viewId, size); + }; } if (newValue != null) { diff --git a/lib/src/track/local/local.dart b/lib/src/track/local/local.dart index e520b8712..e0c711b91 100644 --- a/lib/src/track/local/local.dart +++ b/lib/src/track/local/local.dart @@ -41,21 +41,24 @@ import 'video.dart'; /// Used to group [LocalVideoTrack] and [RemoteVideoTrack]. mixin VideoTrack on Track { @internal - final List viewKeys = []; + final Map viewSizes = {}; @internal - Function(Key)? onVideoViewBuild; + Function(String, Size)? onVideoViewBuild; @internal - GlobalKey addViewKey() { - final key = GlobalKey(); - viewKeys.add(key); - return key; + Function(String, Size)? onViewViewResize; + + @internal + String registerVideoView([Size size = Size.zero]) { + final id = Track.uuid.v4(); + viewSizes[id] = size; + return id; } @internal - void removeViewKey(GlobalKey key) { - viewKeys.remove(key); + void unregisterVideoView(String id) { + viewSizes.remove(id); } } diff --git a/lib/src/widgets/video_track_renderer.dart b/lib/src/widgets/video_track_renderer.dart index 95b683bce..e5525dcef 100644 --- a/lib/src/widgets/video_track_renderer.dart +++ b/lib/src/widgets/video_track_renderer.dart @@ -90,8 +90,8 @@ class _VideoTrackRendererState extends State { bool _rendererReadyForWeb = false; double? _aspectRatio; EventsListener? _listener; - // Used to compute visibility information - late GlobalKey _internalKey; + late String _internalId; + Key get _internalKey => ValueKey('${widget.track.sid}_$_internalId'); Future _initializeRenderer() async { if (lkPlatformIs(PlatformType.iOS) && widget.renderMode == VideoRenderMode.platformView) { @@ -142,7 +142,11 @@ class _VideoTrackRendererState extends State { if (widget.cachedRenderer != null) { _renderer = widget.cachedRenderer; } - _internalKey = widget.track.addViewKey(); + _internalId = widget.track.registerVideoView(); + WidgetsBindingCompatible.instance?.addPostFrameCallback((timeStamp) { + widget.track.onVideoViewBuild?.call(_internalId, _computedSize); + }); + if (kIsWeb) { unawaited(() async { await _initializeRenderer(); @@ -154,7 +158,7 @@ class _VideoTrackRendererState extends State { @override void dispose() { - widget.track.removeViewKey(_internalKey); + widget.track.unregisterVideoView(_internalId); unawaited(_listener?.dispose()); if (widget.autoDisposeRenderer) { disposeRenderer(); @@ -162,6 +166,13 @@ class _VideoTrackRendererState extends State { super.dispose(); } + Size get _computedSize { + if (!mounted) return Size.zero; + final box = context.findRenderObject() as RenderBox?; + if (box == null) return Size.zero; + return box.size * MediaQuery.of(context).devicePixelRatio; + } + Future _attach() async { _renderer?.srcObject = widget.track.mediaStream; await _listener?.dispose(); @@ -188,8 +199,9 @@ class _VideoTrackRendererState extends State { void didUpdateWidget(covariant VideoTrackRenderer oldWidget) { super.didUpdateWidget(oldWidget); if (widget.track != oldWidget.track) { - oldWidget.track.removeViewKey(_internalKey); - _internalKey = widget.track.addViewKey(); + oldWidget.track.unregisterVideoView(_internalId); + _internalId = widget.track.registerVideoView(); + unawaited(() async { await _attach(); }()); @@ -200,14 +212,14 @@ class _VideoTrackRendererState extends State { } } - Widget _videoViewForWeb() => !_rendererReadyForWeb + Widget _videoViewForWeb(Size size) => !_rendererReadyForWeb ? Container() : Builder( key: _internalKey, builder: (ctx) { // let it render before notifying build WidgetsBindingCompatible.instance?.addPostFrameCallback((timeStamp) { - widget.track.onVideoViewBuild?.call(_internalKey); + widget.track.onViewViewResize?.call(_internalId, size); }); return rtc.RTCVideoView( _renderer! as rtc.RTCVideoRenderer, @@ -238,7 +250,7 @@ class _VideoTrackRendererState extends State { ); } - Widget _videoViewForNative() => FutureBuilder( + Widget _videoViewForNative(Size size) => FutureBuilder( future: _initializeRenderer(), builder: (context, snapshot) { if ((snapshot.hasData && _renderer != null) || @@ -248,7 +260,7 @@ class _VideoTrackRendererState extends State { builder: (ctx) { // let it render before notifying build WidgetsBindingCompatible.instance?.addPostFrameCallback((timeStamp) { - widget.track.onVideoViewBuild?.call(_internalKey); + widget.track.onViewViewResize?.call(_internalId, size); }); if (!lkPlatformIsMobile() || widget.track is! LocalVideoTrack) { @@ -278,14 +290,19 @@ class _VideoTrackRendererState extends State { // different rendering methods for web and native. @override Widget build(BuildContext context) { - final child = kIsWeb ? _videoViewForWeb() : _videoViewForNative(); - - if (widget.fit == VideoViewFit.cover) { - return child; - } - final videoView = LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { + final size = constraints.biggest * MediaQuery.of(context).devicePixelRatio; + + final child = kIsWeb ? _videoViewForWeb(size) : _videoViewForNative(size); + + if (widget.fit == VideoViewFit.cover) { + return child; + } + + if (size.isEmpty) { + return child; + } if (!constraints.hasBoundedWidth && !constraints.hasBoundedHeight) { return child; } @@ -316,7 +333,7 @@ class _VideoTrackRendererState extends State { }, ); - if (widget.autoCenter) { + if (widget.autoCenter && widget.fit == VideoViewFit.contain) { return Center(child: videoView); } else { return videoView;