Skip to content

Commit e7ab100

Browse files
committed
cocoa replay fix
1 parent 4ccd5ea commit e7ab100

File tree

3 files changed

+89
-78
lines changed

3 files changed

+89
-78
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import 'dart:async';
2+
import 'dart:typed_data';
3+
4+
import 'package:meta/meta.dart';
5+
6+
import '../../../sentry_flutter.dart';
7+
import '../../replay/replay_recorder.dart';
8+
import '../../screenshot/recorder.dart';
9+
import '../../screenshot/recorder_config.dart';
10+
import '../../screenshot/stabilizer.dart';
11+
12+
@internal
13+
class CocoaReplayRecorder {
14+
final SentryFlutterOptions _options;
15+
final ScreenshotRecorder _recorder;
16+
late final ScreenshotStabilizer<void> _stabilizer;
17+
var _completer = Completer<Uint8List?>();
18+
19+
CocoaReplayRecorder(this._options)
20+
: _recorder =
21+
ReplayScreenshotRecorder(ScreenshotRecorderConfig(), _options) {
22+
_stabilizer = ScreenshotStabilizer(_recorder, _options, (screenshot) async {
23+
final pngData = await screenshot.pngData;
24+
_options.logger(
25+
SentryLevel.debug,
26+
'Replay: captured screenshot ('
27+
'${screenshot.width}x${screenshot.height} pixels, '
28+
'${pngData.lengthInBytes} bytes)');
29+
_completer.complete(pngData.buffer.asUint8List());
30+
});
31+
}
32+
33+
Future<Uint8List?> captureScreenshot() async {
34+
_completer = Completer<Uint8List?>();
35+
_stabilizer.ensureFrameAndAddCallback((msSinceEpoch) {
36+
_stabilizer.capture(msSinceEpoch).onError(_completer.completeError);
37+
});
38+
return _completer.future;
39+
}
40+
}

flutter/lib/src/native/cocoa/sentry_native_cocoa.dart

Lines changed: 5 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,19 @@
11
import 'dart:async';
22
import 'dart:ffi';
3-
import 'dart:typed_data';
43

54
import 'package:meta/meta.dart';
65

76
import '../../../sentry_flutter.dart';
87
import '../../replay/replay_config.dart';
9-
import '../../replay/replay_recorder.dart';
10-
import '../../screenshot/recorder.dart';
11-
import '../../screenshot/recorder_config.dart';
12-
import '../../screenshot/stabilizer.dart';
138
import '../native_memory.dart';
149
import '../sentry_native_channel.dart';
1510
import 'binding.dart' as cocoa;
11+
import 'cocoa_replay_recorder.dart';
1612

1713
@internal
1814
class SentryNativeCocoa extends SentryNativeChannel {
1915
late final _lib = cocoa.SentryCocoa(DynamicLibrary.process());
20-
ScreenshotRecorder? _replayRecorder;
16+
CocoaReplayRecorder? _replayRecorder;
2117
SentryId? _replayId;
2218

2319
SentryNativeCocoa(super.options);
@@ -33,12 +29,12 @@ class SentryNativeCocoa extends SentryNativeChannel {
3329
channel.setMethodCallHandler((call) async {
3430
switch (call.method) {
3531
case 'captureReplayScreenshot':
36-
_replayRecorder ??=
37-
ReplayScreenshotRecorder(ScreenshotRecorderConfig(), options);
32+
_replayRecorder ??= CocoaReplayRecorder(options);
3833

3934
final replayId = call.arguments['replayId'] == null
4035
? null
4136
: SentryId.fromId(call.arguments['replayId'] as String);
37+
4238
if (_replayId != replayId) {
4339
_replayId = replayId;
4440
hub.configureScope((s) {
@@ -47,38 +43,7 @@ class SentryNativeCocoa extends SentryNativeChannel {
4743
});
4844
}
4945

50-
final widgetsBinding = options.bindingUtils.instance;
51-
if (widgetsBinding == null) {
52-
options.logger(SentryLevel.warning,
53-
'Replay: failed to capture screenshot, WidgetsBinding.instance is null');
54-
return null;
55-
}
56-
57-
final completer = Completer<Uint8List?>();
58-
59-
final stabilizer = ScreenshotStabilizer(_replayRecorder!, options,
60-
(screenshot) async {
61-
final pngData = await screenshot.pngData;
62-
options.logger(
63-
SentryLevel.debug,
64-
'Replay: captured screenshot ('
65-
'${screenshot.width}x${screenshot.height} pixels, '
66-
'${pngData.lengthInBytes} bytes)');
67-
completer.complete(pngData.buffer.asUint8List());
68-
});
69-
70-
late final Uint8List? uint8List;
71-
try {
72-
stabilizer.ensureFrameAndAddCallback((msSinceEpoch) {
73-
stabilizer
74-
.capture(msSinceEpoch)
75-
.onError(completer.completeError);
76-
});
77-
uint8List = await completer.future;
78-
} finally {
79-
stabilizer.dispose();
80-
}
81-
46+
final uint8List = await _replayRecorder!.captureScreenshot();
8247
// Malloc memory and copy the data. Native must free it.
8348
return uint8List?.toNativeMemory().toJson();
8449
default:

flutter/lib/src/screenshot/stabilizer.dart

Lines changed: 44 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -64,49 +64,55 @@ class ScreenshotStabilizer<R> {
6464
await prevScreenshot.hasSameImageAs(screenshot)) {
6565
// Sucessfully captured a stable screenshot (repeated at least twice).
6666
_tries = 0;
67-
if (prevScreenshot.flow.id == screenshot.flow.id) {
68-
// If it's from the same (retry) flow, use the first screenshot timestamp.
69-
await _callback(prevScreenshot);
70-
} else {
71-
// Otherwise this was called from a scheduler (in a new flow) so use
72-
// the new timestamp.
73-
await _callback(screenshot);
74-
}
75-
} else if (maxTries != null && _tries >= maxTries!) {
76-
throw Exception('Failed to capture a stable screenshot. '
77-
'Giving up after $_tries tries.');
78-
} else {
79-
// Add a delay to give the UI a chance to stabilize.
80-
// Only do this on every other frame so that there's a greater chance
81-
// of two subsequent frames being the same.
82-
final sleepMs = _tries % 2 == 1 ? min(100, 10 * (_tries - 1)) : 0;
83-
84-
if (_tries > 1) {
85-
_options.logger(
86-
SentryLevel.debug,
87-
'${_recorder.logName}: '
88-
'Retrying screenshot capture due to UI changes. '
89-
'Delay before next capture: $sleepMs ms.');
90-
}
9167

92-
if (sleepMs > 0) {
93-
await Future<void>.delayed(Duration(milliseconds: sleepMs));
94-
}
68+
// If it's from the same (retry) flow, use the first screenshot
69+
// timestamp. Otherwise this was called from a scheduler (in a new flow)
70+
// so use the new timestamp.
71+
await _callback((prevScreenshot.flow.id == screenshot.flow.id)
72+
? prevScreenshot
73+
: screenshot);
9574

96-
final completer = Completer<void>();
97-
ensureFrameAndAddCallback((Duration sinceSchedulerEpoch) async {
98-
_tries++;
99-
try {
100-
await _recorder.capture(_onImageCaptured, screenshot.flow);
101-
completer.complete();
102-
} catch (e, stackTrace) {
103-
completer.completeError(e, stackTrace);
104-
}
105-
});
106-
return completer.future;
75+
// Do not just return the Future resulting from callback().
76+
// We need to await here so that the dispose runs ASAP.
77+
return;
10778
}
10879
} finally {
80+
// Note: we need to dispose (free the memory) before recursion.
10981
prevScreenshot?.dispose();
11082
}
83+
84+
if (maxTries != null && _tries >= maxTries!) {
85+
throw Exception('Failed to capture a stable screenshot. '
86+
'Giving up after $_tries tries.');
87+
} else {
88+
// Add a delay to give the UI a chance to stabilize.
89+
// Only do this on every other frame so that there's a greater chance
90+
// of two subsequent frames being the same.
91+
final sleepMs = _tries % 2 == 1 ? min(100, 10 * (_tries - 1)) : 0;
92+
93+
if (_tries > 1) {
94+
_options.logger(
95+
SentryLevel.debug,
96+
'${_recorder.logName}: '
97+
'Retrying screenshot capture due to UI changes. '
98+
'Delay before next capture: $sleepMs ms.');
99+
}
100+
101+
if (sleepMs > 0) {
102+
await Future<void>.delayed(Duration(milliseconds: sleepMs));
103+
}
104+
105+
final completer = Completer<void>();
106+
ensureFrameAndAddCallback((Duration sinceSchedulerEpoch) async {
107+
_tries++;
108+
try {
109+
await _recorder.capture(_onImageCaptured, screenshot.flow);
110+
completer.complete();
111+
} catch (e, stackTrace) {
112+
completer.completeError(e, stackTrace);
113+
}
114+
});
115+
return completer.future;
116+
}
111117
}
112118
}

0 commit comments

Comments
 (0)