diff --git a/lib/features/desktop/desktop_client.dart b/lib/features/desktop/desktop_client.dart index 284f214b..ebe5b1bc 100644 --- a/lib/features/desktop/desktop_client.dart +++ b/lib/features/desktop/desktop_client.dart @@ -40,6 +40,16 @@ bool desktopShouldDropInputEvent( return event['type'] == 'mouse_move' && bufferedAmount > bufferedAmountLimit; } +bool desktopHasRenderedVideoFrame({ + required bool hasStream, + required int rendererVideoWidth, + required int rendererVideoHeight, + required bool hasDecodedFrames, +}) { + return hasStream && + (hasDecodedFrames || (rendererVideoWidth > 0 && rendererVideoHeight > 0)); +} + String desktopSessionId() { return 'remote-desktop-${randomIdInternal()}'; } diff --git a/lib/features/desktop/desktop_view.dart b/lib/features/desktop/desktop_view.dart index c349a177..b0ff4880 100644 --- a/lib/features/desktop/desktop_view.dart +++ b/lib/features/desktop/desktop_view.dart @@ -55,6 +55,7 @@ class _DesktopViewState extends State { bool _showControlPanel = true; String _connectionState = 'disconnected'; bool _hasStream = false; + bool _hasDecodedVideoFrame = false; bool _isFocused = false; Size _remoteDesktopSize = const Size(1280, 720); @@ -65,10 +66,12 @@ class _DesktopViewState extends State { StreamSubscription? _stateSubscription; Timer? _firstFrameStatsTimer; - bool get _hasVideoFrame => - _hasStream && - _localRenderer.videoWidth > 0 && - _localRenderer.videoHeight > 0; + bool get _hasVideoFrame => desktopHasRenderedVideoFrame( + hasStream: _hasStream, + rendererVideoWidth: _localRenderer.videoWidth, + rendererVideoHeight: _localRenderer.videoHeight, + hasDecodedFrames: _hasDecodedVideoFrame, + ); @override void initState() { @@ -91,6 +94,7 @@ class _DesktopViewState extends State { setState(() { _localRenderer.srcObject = stream; _hasStream = true; + _hasDecodedVideoFrame = false; }); _startFirstFrameDiagnostics(); } @@ -103,6 +107,7 @@ class _DesktopViewState extends State { if (_connectionState == 'disconnected' || _connectionState == 'failed') { _hasStream = false; + _hasDecodedVideoFrame = false; _localRenderer.srcObject = null; _stopFirstFrameDiagnostics(); } @@ -114,6 +119,9 @@ class _DesktopViewState extends State { Future _initRenderer() async { await _localRenderer.initialize(); _localRenderer.onResize = () { + if (_localRenderer.videoWidth > 0 && _localRenderer.videoHeight > 0) { + _hasDecodedVideoFrame = true; + } _stopFirstFrameDiagnostics(); if (mounted) { setState(() {}); @@ -123,25 +131,35 @@ class _DesktopViewState extends State { void _startFirstFrameDiagnostics() { _firstFrameStatsTimer?.cancel(); - _firstFrameStatsTimer = Timer.periodic(const Duration(seconds: 5), (_) { + unawaited(_collectFirstFrameStats()); + _firstFrameStatsTimer = Timer.periodic(const Duration(seconds: 2), (_) { if (!_hasStream || _hasVideoFrame || !mounted) { _stopFirstFrameDiagnostics(); return; } - unawaited(() async { - try { - final stats = await _client.collectVideoStats(); - if (stats == null) { - return; - } - debugPrint('Remote desktop waiting for first frame: $stats'); - } catch (error) { - debugPrint('Remote desktop stats failed: $error'); - } - }()); + unawaited(_collectFirstFrameStats()); }); } + Future _collectFirstFrameStats() async { + try { + final stats = await _client.collectVideoStats(); + if (stats == null || !mounted || !_hasStream) { + return; + } + if (stats.hasDecodedFrames) { + setState(() { + _hasDecodedVideoFrame = true; + }); + _stopFirstFrameDiagnostics(); + return; + } + debugPrint('Remote desktop waiting for first frame: $stats'); + } catch (error) { + debugPrint('Remote desktop stats failed: $error'); + } + } + void _stopFirstFrameDiagnostics() { _firstFrameStatsTimer?.cancel(); _firstFrameStatsTimer = null; diff --git a/test/features/desktop/desktop_client_test.dart b/test/features/desktop/desktop_client_test.dart index 1a104d3f..76ae0c41 100644 --- a/test/features/desktop/desktop_client_test.dart +++ b/test/features/desktop/desktop_client_test.dart @@ -177,6 +177,36 @@ void main() { ); }); + test('treats decoded video stats as a rendered first frame', () { + expect( + desktopHasRenderedVideoFrame( + hasStream: true, + rendererVideoWidth: 0, + rendererVideoHeight: 0, + hasDecodedFrames: true, + ), + isTrue, + ); + expect( + desktopHasRenderedVideoFrame( + hasStream: true, + rendererVideoWidth: 1280, + rendererVideoHeight: 720, + hasDecodedFrames: false, + ), + isTrue, + ); + expect( + desktopHasRenderedVideoFrame( + hasStream: false, + rendererVideoWidth: 1280, + rendererVideoHeight: 720, + hasDecodedFrames: true, + ), + isFalse, + ); + }); + test('uses bridge-provided remote stream when present', () async { var fallbackCreated = false; final providedStream = FakeMediaStream('provided-stream');