From 08d5c546ce6fc430d3b3a1dd7116492a99640479 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Wed, 15 Apr 2026 17:17:43 -0300 Subject: [PATCH 1/6] fix: Requested video size should be multiplied by device pixel ration --- lib/src/publication/remote.dart | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/lib/src/publication/remote.dart b/lib/src/publication/remote.dart index f9e30bb5e..deaa3e9f6 100644 --- a/lib/src/publication/remote.dart +++ b/lib/src/publication/remote.dart @@ -16,7 +16,6 @@ import 'dart:async'; import 'dart:math'; import 'package:flutter/widgets.dart'; - import 'package:meta/meta.dart'; import '../core/signal_client.dart'; @@ -149,25 +148,24 @@ class RemoteTrackPublication extends TrackPublication 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 (var key in videoTrack.viewKeys) { + final context = key.currentContext; + if (context == null) continue; + final renderBox = context.findRenderObject() as RenderBox?; + if (renderBox == null || !renderBox.hasSize) continue; + videoViewsSizes.add(renderBox.size * MediaQuery.of(context).devicePixelRatio); + } - 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.ceil() + ..height = largestVideoView.height.ceil(); } // Only send new settings to server if it changed From 1bdae49e42aee7e96cfea45e5bf22077dd9c4725 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Wed, 15 Apr 2026 18:07:42 -0300 Subject: [PATCH 2/6] fix: Do not use pooling to get the size of each view. --- lib/src/publication/remote.dart | 53 ++++++++++------------- lib/src/track/local/local.dart | 20 +++++---- lib/src/widgets/video_track_renderer.dart | 41 ++++++++++-------- 3 files changed, 59 insertions(+), 55 deletions(-) diff --git a/lib/src/publication/remote.dart b/lib/src/publication/remote.dart index deaa3e9f6..83b86e4d4 100644 --- a/lib/src/publication/remote.dart +++ b/lib/src/publication/remote.dart @@ -96,7 +96,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; @@ -111,7 +110,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(); }); @@ -132,35 +130,36 @@ 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), + ); + + /// 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] == size) return; - final videoTrack = track as VideoTrack; + logger.finer('[Visibility] VideoView did resize'); + videoTrack.viewSizes[viewViewId] = size; final settings = lk_rtc.UpdateTrackSettings( trackSids: [sid], disabled: true, ); - final videoViewsSizes = []; - for (var key in videoTrack.viewKeys) { - final context = key.currentContext; - if (context == null) continue; - final renderBox = context.findRenderObject() as RenderBox?; - if (renderBox == null || !renderBox.hasSize) continue; - videoViewsSizes.add(renderBox.size * MediaQuery.of(context).devicePixelRatio); - } + final videoViewsSizes = [ + for (final MapEntry(key: _, value: size) in videoTrack.viewSizes.entries) + if (size > Size.zero) size, + ]; logger.finer('[Visibility] ${track?.sid} watching ${videoViewsSizes.length} views...'); if (videoViewsSizes.isNotEmpty) { - final largestVideoView = videoViewsSizes.reduce((value, element) => maxOfSizes(value, element)); + final largestVideoView = videoViewsSizes.reduce((value, element) => _maxOfSizes(value, element)); settings ..disabled = false @@ -194,24 +193,20 @@ 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) { + updateVideoViewSize(viewId, size); + }; } if (newValue != null) { diff --git a/lib/src/track/local/local.dart b/lib/src/track/local/local.dart index e520b8712..0d7dc2368 100644 --- a/lib/src/track/local/local.dart +++ b/lib/src/track/local/local.dart @@ -16,7 +16,6 @@ import 'dart:async'; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; - import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc; import 'package:meta/meta.dart'; @@ -41,21 +40,24 @@ import 'video.dart'; /// Used to group [LocalVideoTrack] and [RemoteVideoTrack]. mixin VideoTrack on Track { @internal - final List viewKeys = []; + final Map viewSizes = {}; + + @internal + Function(String, Size)? onVideoViewBuild; @internal - Function(Key)? onVideoViewBuild; + Function(String, Size)? onViewViewResize; @internal - GlobalKey addViewKey() { - final key = GlobalKey(); - viewKeys.add(key); - return key; + String registerVideoView(Size size) { + 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..8df07e07c 100644 --- a/lib/src/widgets/video_track_renderer.dart +++ b/lib/src/widgets/video_track_renderer.dart @@ -17,7 +17,6 @@ import 'dart:math'; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; - import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc; import '../events.dart'; @@ -90,8 +89,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 +141,7 @@ class _VideoTrackRendererState extends State { if (widget.cachedRenderer != null) { _renderer = widget.cachedRenderer; } - _internalKey = widget.track.addViewKey(); + _internalId = widget.track.registerVideoView(Size.zero); if (kIsWeb) { unawaited(() async { await _initializeRenderer(); @@ -154,7 +153,7 @@ class _VideoTrackRendererState extends State { @override void dispose() { - widget.track.removeViewKey(_internalKey); + widget.track.unregisterVideoView(_internalId); unawaited(_listener?.dispose()); if (widget.autoDisposeRenderer) { disposeRenderer(); @@ -188,8 +187,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(Size.zero); + unawaited(() async { await _attach(); }()); @@ -200,14 +200,15 @@ 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.onVideoViewBuild?.call(_internalId, size); + widget.track.onViewViewResize?.call(_internalId, size); }); return rtc.RTCVideoView( _renderer! as rtc.RTCVideoRenderer, @@ -238,7 +239,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 +249,8 @@ class _VideoTrackRendererState extends State { builder: (ctx) { // let it render before notifying build WidgetsBindingCompatible.instance?.addPostFrameCallback((timeStamp) { - widget.track.onVideoViewBuild?.call(_internalKey); + widget.track.onVideoViewBuild?.call(_internalId, size); + widget.track.onViewViewResize?.call(_internalId, size); }); if (!lkPlatformIsMobile() || widget.track is! LocalVideoTrack) { @@ -278,14 +280,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; } From c2d75ae82684a77e1d077f90ead2d65a51063186 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Wed, 15 Apr 2026 18:32:07 -0300 Subject: [PATCH 3/6] fix: Correctly initialize view --- lib/src/publication/remote.dart | 3 ++- lib/src/track/local/local.dart | 2 +- lib/src/widgets/video_track_renderer.dart | 17 +++++++++++++---- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/lib/src/publication/remote.dart b/lib/src/publication/remote.dart index 83b86e4d4..d4d110b4b 100644 --- a/lib/src/publication/remote.dart +++ b/lib/src/publication/remote.dart @@ -143,7 +143,7 @@ class RemoteTrackPublication extends TrackPublication if (videoTrack == null) return; if (videoTrack.viewSizes[viewViewId] == size) return; - logger.finer('[Visibility] VideoView did resize'); + print('[Visibility] VideoView did resize to ${size.width}x${size.height}, quick: ${quick}'); videoTrack.viewSizes[viewViewId] = size; final settings = lk_rtc.UpdateTrackSettings( @@ -205,6 +205,7 @@ class RemoteTrackPublication extends TrackPublication } }; newValue.onViewViewResize = (viewId, size) { + // schedule update to server updateVideoViewSize(viewId, size); }; } diff --git a/lib/src/track/local/local.dart b/lib/src/track/local/local.dart index 0d7dc2368..fbd5d8729 100644 --- a/lib/src/track/local/local.dart +++ b/lib/src/track/local/local.dart @@ -49,7 +49,7 @@ mixin VideoTrack on Track { Function(String, Size)? onViewViewResize; @internal - String registerVideoView(Size size) { + String registerVideoView([Size size = Size.zero]) { final id = Track.uuid.v4(); viewSizes[id] = size; return id; diff --git a/lib/src/widgets/video_track_renderer.dart b/lib/src/widgets/video_track_renderer.dart index 8df07e07c..96353e555 100644 --- a/lib/src/widgets/video_track_renderer.dart +++ b/lib/src/widgets/video_track_renderer.dart @@ -141,7 +141,11 @@ class _VideoTrackRendererState extends State { if (widget.cachedRenderer != null) { _renderer = widget.cachedRenderer; } - _internalId = widget.track.registerVideoView(Size.zero); + _internalId = widget.track.registerVideoView(); + WidgetsBindingCompatible.instance?.addPostFrameCallback((timeStamp) { + widget.track.onVideoViewBuild?.call(_internalId, _computedSize); + }); + if (kIsWeb) { unawaited(() async { await _initializeRenderer(); @@ -161,6 +165,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,7 +199,7 @@ class _VideoTrackRendererState extends State { super.didUpdateWidget(oldWidget); if (widget.track != oldWidget.track) { oldWidget.track.unregisterVideoView(_internalId); - _internalId = widget.track.registerVideoView(Size.zero); + _internalId = widget.track.registerVideoView(); unawaited(() async { await _attach(); @@ -207,7 +218,6 @@ class _VideoTrackRendererState extends State { builder: (ctx) { // let it render before notifying build WidgetsBindingCompatible.instance?.addPostFrameCallback((timeStamp) { - widget.track.onVideoViewBuild?.call(_internalId, size); widget.track.onViewViewResize?.call(_internalId, size); }); return rtc.RTCVideoView( @@ -249,7 +259,6 @@ class _VideoTrackRendererState extends State { builder: (ctx) { // let it render before notifying build WidgetsBindingCompatible.instance?.addPostFrameCallback((timeStamp) { - widget.track.onVideoViewBuild?.call(_internalId, size); widget.track.onViewViewResize?.call(_internalId, size); }); From 9730b9dc17f0862a1194f013b37e3e5ba60da95c Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Wed, 15 Apr 2026 18:34:57 -0300 Subject: [PATCH 4/6] fix: Warning if view is unregistered --- lib/src/publication/remote.dart | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/src/publication/remote.dart b/lib/src/publication/remote.dart index d4d110b4b..fadd18f4e 100644 --- a/lib/src/publication/remote.dart +++ b/lib/src/publication/remote.dart @@ -141,15 +141,18 @@ class RemoteTrackPublication extends TrackPublication 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; 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, - ); + final settings = lk_rtc.UpdateTrackSettings(trackSids: [sid], disabled: true); final videoViewsSizes = [ for (final MapEntry(key: _, value: size) in videoTrack.viewSizes.entries) @@ -163,8 +166,8 @@ class RemoteTrackPublication extends TrackPublication settings ..disabled = false - ..width = largestVideoView.width.ceil() - ..height = largestVideoView.height.ceil(); + ..width = largestVideoView.width.round() + ..height = largestVideoView.height.round(); } // Only send new settings to server if it changed From 6ee20ba7487bdd595e4ec9d13016617b990adca0 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Wed, 15 Apr 2026 18:45:24 -0300 Subject: [PATCH 5/6] fix: Only auto center if video fit is contain --- lib/src/widgets/video_track_renderer.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/widgets/video_track_renderer.dart b/lib/src/widgets/video_track_renderer.dart index 96353e555..cef5736f7 100644 --- a/lib/src/widgets/video_track_renderer.dart +++ b/lib/src/widgets/video_track_renderer.dart @@ -332,7 +332,7 @@ class _VideoTrackRendererState extends State { }, ); - if (widget.autoCenter) { + if (widget.autoCenter && widget.fit == VideoViewFit.contain) { return Center(child: videoView); } else { return videoView; From f735c4b7094212137217dab90e946cfc4d76168d Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Wed, 15 Apr 2026 19:00:03 -0300 Subject: [PATCH 6/6] chore: linting --- lib/src/publication/remote.dart | 1 + lib/src/track/local/local.dart | 1 + lib/src/widgets/video_track_renderer.dart | 1 + 3 files changed, 3 insertions(+) diff --git a/lib/src/publication/remote.dart b/lib/src/publication/remote.dart index fadd18f4e..be82dcef1 100644 --- a/lib/src/publication/remote.dart +++ b/lib/src/publication/remote.dart @@ -16,6 +16,7 @@ import 'dart:async'; import 'dart:math'; import 'package:flutter/widgets.dart'; + import 'package:meta/meta.dart'; import '../core/signal_client.dart'; diff --git a/lib/src/track/local/local.dart b/lib/src/track/local/local.dart index fbd5d8729..e0c711b91 100644 --- a/lib/src/track/local/local.dart +++ b/lib/src/track/local/local.dart @@ -16,6 +16,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; + import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc; import 'package:meta/meta.dart'; diff --git a/lib/src/widgets/video_track_renderer.dart b/lib/src/widgets/video_track_renderer.dart index cef5736f7..e5525dcef 100644 --- a/lib/src/widgets/video_track_renderer.dart +++ b/lib/src/widgets/video_track_renderer.dart @@ -17,6 +17,7 @@ import 'dart:math'; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; + import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc; import '../events.dart';