Skip to content

Commit c6b7cb8

Browse files
authored
refactor: workaround capture issue (#2657)
* refactor: remove ScreenshotStabilizer * test: fixup native test * workaround flutter image issue * fixup * try to fix ci
1 parent 8f62960 commit c6b7cb8

File tree

7 files changed

+48
-236
lines changed

7 files changed

+48
-236
lines changed

flutter/lib/src/event_processor/screenshot_event_processor.dart

Lines changed: 2 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import '../screenshot/recorder.dart';
99
import '../screenshot/recorder_config.dart';
1010
import 'package:flutter/widgets.dart' as widget;
1111

12-
import '../screenshot/stabilizer.dart';
1312
import '../utils/debouncer.dart';
1413

1514
class ScreenshotEventProcessor implements EventProcessor {
@@ -126,37 +125,6 @@ class ScreenshotEventProcessor implements EventProcessor {
126125
}
127126

128127
@internal
129-
Future<Uint8List?> createScreenshot() async {
130-
if (_options.experimental.privacyForScreenshots == null) {
131-
return _recorder.capture((screenshot) =>
132-
screenshot.pngData.then((v) => v.buffer.asUint8List()));
133-
} else {
134-
// If masking is enabled, we need to use [ScreenshotStabilizer].
135-
final completer = Completer<Uint8List?>();
136-
final stabilizer = ScreenshotStabilizer(
137-
_recorder, _options,
138-
(screenshot) async {
139-
final pngData = await screenshot.pngData;
140-
completer.complete(pngData.buffer.asUint8List());
141-
},
142-
// This limits the amount of time to take a stable masked screenshot.
143-
maxTries: 5,
144-
// We need to force the frame the frame or this could hang indefinitely.
145-
frameSchedulingMode: FrameSchedulingMode.forced,
146-
);
147-
try {
148-
unawaited(
149-
stabilizer.capture(Duration.zero).onError(completer.completeError));
150-
// DO NOT return completer.future directly - we need to dispose first.
151-
return await completer.future.timeout(const Duration(seconds: 1),
152-
onTimeout: () {
153-
_options.logger(
154-
SentryLevel.warning, 'Timed out taking a stable screenshot.');
155-
return null;
156-
});
157-
} finally {
158-
stabilizer.dispose();
159-
}
160-
}
161-
}
128+
Future<Uint8List?> createScreenshot() => _recorder.capture(
129+
(screenshot) => screenshot.pngData.then((v) => v.buffer.asUint8List()));
162130
}

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

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,24 @@ import '../../../sentry_flutter.dart';
66
import '../../replay/replay_recorder.dart';
77
import '../../screenshot/recorder.dart';
88
import '../../screenshot/recorder_config.dart';
9-
import '../../screenshot/stabilizer.dart';
109
import '../native_memory.dart';
1110

1211
@internal
1312
class CocoaReplayRecorder {
1413
final SentryFlutterOptions _options;
1514
final ScreenshotRecorder _recorder;
16-
late final ScreenshotStabilizer<void> _stabilizer;
17-
var _completer = Completer<Map<String, int>?>();
1815

1916
CocoaReplayRecorder(this._options)
2017
: _recorder = ReplayScreenshotRecorder(
21-
ScreenshotRecorderConfig(
22-
pixelRatio:
23-
_options.experimental.replay.quality.resolutionScalingFactor,
24-
),
25-
_options) {
26-
_stabilizer = ScreenshotStabilizer(_recorder, _options, (screenshot) async {
18+
ScreenshotRecorderConfig(
19+
pixelRatio:
20+
_options.experimental.replay.quality.resolutionScalingFactor,
21+
),
22+
_options,
23+
);
24+
25+
Future<Map<String, int>?> captureScreenshot() async {
26+
return _recorder.capture((screenshot) async {
2727
final data = await screenshot.rawRgbaData;
2828
_options.logger(
2929
SentryLevel.debug,
@@ -35,15 +35,7 @@ class CocoaReplayRecorder {
3535
final json = data.toNativeMemory().toJson();
3636
json['width'] = screenshot.width;
3737
json['height'] = screenshot.height;
38-
_completer.complete(json);
39-
});
40-
}
41-
42-
Future<Map<String, int>?> captureScreenshot() async {
43-
_completer = Completer();
44-
_stabilizer.ensureFrameAndAddCallback((msSinceEpoch) {
45-
_stabilizer.capture(msSinceEpoch).onError(_completer.completeError);
38+
return json;
4639
});
47-
return _completer.future;
4840
}
4941
}

flutter/lib/src/replay/scheduled_recorder.dart

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import 'dart:async';
22

3+
import 'package:flutter/scheduler.dart';
34
import 'package:meta/meta.dart';
45

56
import '../../sentry_flutter.dart';
6-
import '../screenshot/stabilizer.dart';
77
import '../screenshot/screenshot.dart';
88
import 'replay_recorder.dart';
99
import 'scheduled_recorder_config.dart';
@@ -19,7 +19,6 @@ class ScheduledScreenshotRecorder extends ReplayScreenshotRecorder {
1919
late final ScheduledScreenshotRecorderCallback _callback;
2020
var _status = _Status.running;
2121
late final Duration _frameDuration;
22-
late final ScreenshotStabilizer<void> _stabilizer;
2322
// late final _idleFrameFiller = _IdleFrameFiller(_frameDuration, _onScreenshot);
2423

2524
@override
@@ -35,15 +34,23 @@ class ScheduledScreenshotRecorder extends ReplayScreenshotRecorder {
3534
_frameDuration = Duration(milliseconds: 1000 ~/ config.frameRate);
3635
assert(_frameDuration.inMicroseconds > 0);
3736

38-
_stabilizer = ScreenshotStabilizer(this, options, _onImageCaptured);
39-
_scheduler = Scheduler(_frameDuration, _stabilizer.capture,
40-
_stabilizer.ensureFrameAndAddCallback);
37+
_scheduler = Scheduler(
38+
_frameDuration,
39+
(_) => capture(_onImageCaptured),
40+
_addPostFrameCallback,
41+
);
4142

4243
if (callback != null) {
4344
_callback = callback;
4445
}
4546
}
4647

48+
void _addPostFrameCallback(FrameCallback callback) {
49+
options.bindingUtils.instance!
50+
..ensureVisualUpdate()
51+
..addPostFrameCallback(callback);
52+
}
53+
4754
set callback(ScheduledScreenshotRecorderCallback callback) {
4855
_callback = callback;
4956
}
@@ -63,12 +70,10 @@ class ScheduledScreenshotRecorder extends ReplayScreenshotRecorder {
6370
}
6471

6572
Future<void> _stopScheduler() {
66-
_stabilizer.stopped = true;
6773
return _scheduler.stop();
6874
}
6975

7076
void _startScheduler() {
71-
_stabilizer.stopped = false;
7277
_scheduler.start();
7378

7479
// We need to schedule a frame because if this happens in-between user
@@ -82,7 +87,6 @@ class ScheduledScreenshotRecorder extends ReplayScreenshotRecorder {
8287
options.logger(SentryLevel.debug, "$logName: stopping capture.");
8388
_status = _Status.stopped;
8489
await _stopScheduler();
85-
_stabilizer.dispose();
8690
// await Future.wait([_stopScheduler(), _idleFrameFiller.stop()]);
8791
options.logger(SentryLevel.debug, "$logName: capture stopped.");
8892
}

flutter/lib/src/screenshot/recorder.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,18 @@ class _Capture<R> {
190190
final recorder = PictureRecorder();
191191
final canvas = Canvas(recorder);
192192
final image = await futureImage;
193+
194+
// Note: there's a weird bug when we write image to canvas directly.
195+
// If the UI is updating quickly in some apps, the image could get
196+
// out-of-sync with the UI and/or it can get completely mangled.
197+
// This can be reproduced, for example, by switching between Spotube's
198+
// Search vs Library (2nd and 3rd bottom bar buttons).
199+
// Weirdly, dumping the image data seems to prevent this issue...
200+
{
201+
// we do so in a block so it can be GC'ed early.
202+
final _ = await image.toByteData();
203+
}
204+
193205
try {
194206
canvas.drawImage(image, Offset.zero, Paint());
195207
} finally {

flutter/lib/src/screenshot/stabilizer.dart

Lines changed: 0 additions & 147 deletions
This file was deleted.

flutter/test/event_processor/screenshot_event_processor_test.dart

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -68,27 +68,6 @@ void main() {
6868
await _addScreenshotAttachment(tester, null, added: true, isWeb: false);
6969
});
7070

71-
testWidgets('does not block if the screenshot fails to stabilize',
72-
(tester) async {
73-
fixture.options.automatedTestMode = false;
74-
fixture.options.experimental.privacy.maskAllText = true;
75-
// Run with real async https://stackoverflow.com/a/54021863
76-
await tester.runAsync(() async {
77-
final sut = fixture.getSut(null, false);
78-
79-
await tester.pumpWidget(SentryScreenshotWidget(
80-
child: Text('Catching Pokémon is a snap!',
81-
textDirection: TextDirection.ltr)));
82-
83-
final throwable = Exception();
84-
event = SentryEvent(throwable: throwable);
85-
hint = Hint();
86-
await sut.apply(event, hint);
87-
88-
expect(hint.screenshot, isNull);
89-
});
90-
});
91-
9271
testWidgets('adds screenshot attachment with canvasKit renderer',
9372
(tester) async {
9473
await _addScreenshotAttachment(tester, FlutterRenderer.canvasKit,

0 commit comments

Comments
 (0)