11using ControlR . DesktopClient . Common . Models ;
22using ControlR . DesktopClient . Common . ServiceInterfaces ;
33using ControlR . Libraries . NativeInterop . Linux ;
4+ using ControlR . Libraries . Shared . Extensions ;
45using Microsoft . Extensions . Logging ;
56using SkiaSharp ;
67using System . Collections . Concurrent ;
@@ -34,22 +35,18 @@ internal class ScreenGrabberWayland(
3435 ILogger < ScreenGrabberWayland > logger ) : IScreenGrabber
3536{
3637 private const string CaptureModePipeWire = "WaylandPipeWire" ;
37- private const int StreamStartPollingIntervalMs = 100 ;
38- private const int StreamStartTimeoutMs = 3_000 ;
39- private static readonly TimeSpan _recoveryCooldown = TimeSpan . FromSeconds ( 2 ) ;
40- private static readonly TimeSpan _streamStaleThreshold = TimeSpan . FromSeconds ( 2 ) ;
4138
4239 private readonly IDisplayManagerWayland _displayManager = displayManager ;
4340 private readonly SemaphoreSlim _initLock = new ( 1 , 1 ) ;
4441 private readonly ILogger < ScreenGrabberWayland > _logger = logger ;
42+ private readonly TimeSpan _streamStartPollingInterval = TimeSpan . FromMilliseconds ( 100 ) ;
43+ private readonly TimeSpan _streamStartTimeout = TimeSpan . FromSeconds ( 3 ) ;
4544 private readonly TimeProvider _timeProvider = timeProvider ;
4645
4746 private bool _disposed ;
4847 private bool _isInitialized ;
49- private long _lastRecoveryAttemptUtcTicks ;
5048 private ConcurrentDictionary < string , PipeWireStream > _streams = new ( ) ;
5149
52-
5350 public async Task < CaptureResult > CaptureAllDisplays ( bool captureCursor = true )
5451 {
5552 try
@@ -217,37 +214,13 @@ public async Task Initialize(CancellationToken cancellationToken)
217214 }
218215 }
219216
220- internal static bool ShouldRecoverStream (
221- DateTime nowUtc ,
222- DateTime createdUtc ,
223- DateTime ? lastFrameReceivedUtc ,
224- TimeSpan staleThreshold ,
225- DateTime ? lastRecoveryAttemptUtc ,
226- TimeSpan recoveryCooldown )
227- {
228- var mostRecentActivityUtc = lastFrameReceivedUtc ?? createdUtc ;
229- if ( nowUtc - mostRecentActivityUtc < staleThreshold )
230- {
231- return false ;
232- }
233-
234- if ( lastRecoveryAttemptUtc is not null && nowUtc - lastRecoveryAttemptUtc < recoveryCooldown )
235- {
236- return false ;
237- }
238-
239- return true ;
240- }
241-
242217 private async Task < CaptureResult > CaptureStream (
243218 string deviceName ,
244219 PipeWireStream stream ,
245220 CancellationToken cancellationToken )
246221 {
247222 try
248223 {
249- stream = await RefreshStreamIfStale ( deviceName , stream , cancellationToken ) ;
250-
251224 if ( ! stream . TryGetLatestFrame ( out var frameData ) )
252225 {
253226 return CaptureResult . NoChanges ( CaptureModePipeWire ) ;
@@ -306,47 +279,6 @@ private void DisposeStreams()
306279 _streams . Clear ( ) ;
307280 }
308281
309- private DateTime ? GetLastRecoveryAttemptUtc ( )
310- {
311- var ticks = Interlocked . Read ( ref _lastRecoveryAttemptUtcTicks ) ;
312- return ticks > 0
313- ? new DateTime ( ticks , DateTimeKind . Utc )
314- : null ;
315- }
316-
317- private async Task < PipeWireStream > RefreshStreamIfStale (
318- string deviceName ,
319- PipeWireStream stream ,
320- CancellationToken cancellationToken )
321- {
322- var nowUtc = _timeProvider . GetUtcNow ( ) . UtcDateTime ;
323- if ( ! ShouldRecoverStream (
324- nowUtc ,
325- stream . CreatedUtc ,
326- stream . LastFrameReceivedUtc ,
327- _streamStaleThreshold ,
328- GetLastRecoveryAttemptUtc ( ) ,
329- _recoveryCooldown ) )
330- {
331- return stream ;
332- }
333-
334- if ( ! await TryRefreshStreams (
335- deviceName ,
336- $ "stream has not produced a new frame since { ( stream . LastFrameReceivedUtc ?? stream . CreatedUtc ) : O} ",
337- cancellationToken ) )
338- {
339- return stream ;
340- }
341-
342- if ( _streams . TryGetValue ( deviceName , out var refreshedStream ) )
343- {
344- return refreshedStream ;
345- }
346-
347- return _streams . Values . FirstOrDefault ( ) ?? stream ;
348- }
349-
350282 private void ReplaceStreams ( IReadOnlyDictionary < string , PipeWireStream > replacementStreams )
351283 {
352284 var newStreams = new ConcurrentDictionary < string , PipeWireStream > ( replacementStreams ) ;
@@ -371,77 +303,6 @@ private void ReplaceStreams(IReadOnlyDictionary<string, PipeWireStream> replacem
371303 originalStreams . Clear ( ) ;
372304 }
373305
374- private async Task < bool > TryRefreshStreams (
375- string deviceName ,
376- string reason ,
377- CancellationToken cancellationToken )
378- {
379- if ( _disposed )
380- {
381- return false ;
382- }
383-
384- await _initLock . WaitAsync ( cancellationToken ) ;
385- try
386- {
387- var nowUtc = _timeProvider . GetUtcNow ( ) . UtcDateTime ;
388- if ( GetLastRecoveryAttemptUtc ( ) is { } lastRecoveryAttemptUtc &&
389- nowUtc - lastRecoveryAttemptUtc < _recoveryCooldown )
390- {
391- return false ;
392- }
393-
394- Interlocked . Exchange ( ref _lastRecoveryAttemptUtcTicks , nowUtc . Ticks ) ;
395-
396- _logger . LogWarning (
397- "Refreshing Wayland PipeWire streams for display {DeviceName} because {Reason}." ,
398- deviceName ,
399- reason ) ;
400-
401- var replacementStreams = await _displayManager . CreatePipeWireStreams (
402- forcePortalReinitialize : true ,
403- cancellationToken : cancellationToken ) ;
404-
405- if ( replacementStreams . Count == 0 )
406- {
407- _logger . LogWarning ( "Failed to refresh Wayland PipeWire streams. No replacement streams were created." ) ;
408- return false ;
409- }
410-
411- try
412- {
413- await WaitForStreamsReady ( replacementStreams , cancellationToken ) ;
414- ReplaceStreams ( replacementStreams ) ;
415- _isInitialized = ! _streams . IsEmpty ;
416-
417- _logger . LogInformation ( "Wayland PipeWire streams refreshed successfully." ) ;
418- return true ;
419- }
420- catch
421- {
422- foreach ( var stream in replacementStreams . Values )
423- {
424- stream . Dispose ( ) ;
425- }
426-
427- throw ;
428- }
429- }
430- catch ( OperationCanceledException )
431- {
432- throw ;
433- }
434- catch ( Exception ex )
435- {
436- _logger . LogWarning ( ex , "Error while refreshing Wayland PipeWire streams." ) ;
437- return false ;
438- }
439- finally
440- {
441- _initLock . Release ( ) ;
442- }
443- }
444-
445306 private async Task WaitForStreamsReady (
446307 IReadOnlyDictionary < string , PipeWireStream > streams ,
447308 CancellationToken cancellationToken )
@@ -451,15 +312,19 @@ private async Task WaitForStreamsReady(
451312 var deviceName = kv . Key ;
452313 var stream = kv . Value ;
453314
454- for ( var j = 0 ; j < StreamStartTimeoutMs / StreamStartPollingIntervalMs ; j ++ )
315+ using var timer = new PeriodicTimer ( _streamStartPollingInterval , _timeProvider ) ;
316+ using var cts = CancellationTokenSource . CreateLinkedTokenSource ( cancellationToken ) ;
317+ cts . CancelAfter ( _streamStartTimeout ) ;
318+
319+ while ( await timer . WaitForNextTick ( throwOnCancellation : false , cts . Token ) )
455320 {
456321 if ( stream . IsStreaming && stream . TryGetLatestFrame ( out var frameData ) )
457322 {
458323 frameData . Dispose ( ) ;
459324 break ;
460325 }
461326
462- await Task . Delay ( StreamStartPollingIntervalMs , cancellationToken ) ;
327+ await Task . Delay ( _streamStartPollingInterval , cancellationToken ) ;
463328 }
464329
465330 if ( stream . Width > 0 && stream . Height > 0 )
0 commit comments