initialize static method
void
initialize()
Registers the Dart masking handler so the native SDK can request
pre-masked frames. Call once at app startup (e.g. in main()).
Masking BEHAVIOUR — maskAllTexts / maskAllImages / textsToMask — is
configured on CXSessionReplayOptions and applied automatically by
CxFlutterPlugin.initializeSessionReplay. You do NOT set masking here;
CXSessionReplayOptions is the single source of truth.
Implementation
static void initialize() {
_installFrameTracker();
methodChannel.setMethodCallHandler((call) async {
switch (call.method) {
case 'getMaskRegions':
final ids = (call.arguments as List).cast<String>();
return _getMaskRegions(ids);
case 'captureMaskedFlutterView':
// Hold the last good frame while a route transition animates. The
// rasterised bitmap (committed layer) and the mask rects (live render
// tree) can momentarily describe different routes mid-navigation —
// image of one screen, masks of another — producing misaligned masks.
// Mirrors the iOS native isNavigationTransitionActive() skip. This
// applies even to click frames: a mid-transition click frame would be
// misaligned, so the last good (settled) frame is the right thing.
if (_isNavigationTransitionActive()) {
_clickFramePending = false;
return _lastCapture;
}
// Capture ONLY when the UI is fully settled. While anything is
// animating (scroll, fling, ripple, transition) the committed layer
// and the live render tree can describe different scroll/layout states
// — capturing then bakes masks at the wrong offset (text rendered
// un-masked). So skip and reuse the last good frame until the UI is
// still. A click frame is the one exception: the user just interacted,
// its ripple does not move layout, and it must not be dropped — but a
// click that starts a route transition is already handled by the
// navigation guard above (and the post-frame re-check).
final isClickFrame = _clickFramePending;
_clickFramePending = false;
if (!isClickFrame &&
_shouldSkip(_captureInFlight,
_sinceLastFrame.elapsedMilliseconds)) {
return _lastCapture;
}
_captureInFlight++;
try {
final source = FlutterFrameSource.forImplicitView(
maskCollector: SessionReplayMasking.collectAllMaskRectsSync,
captureScale: _captureScale,
);
if (source == null) return _lastCapture;
final frame = await _captureFrameSynced(source);
final masked = await _compositeMasks(frame.image, frame.rects);
try {
// Debug-only: dump the exact Dart-masked frame to disk so the
// masking can be validated locally (mirrors the iOS native
// Documents/SessionReplay debug save).
await _saveMaskedFrameForDebug(masked);
final byteData =
await masked.toByteData(format: ui.ImageByteFormat.rawRgba);
if (byteData == null) return _lastCapture;
_lastCapture = {
'bytes': byteData.buffer.asUint8List(),
'width': masked.width,
'height': masked.height,
};
return _lastCapture;
} finally {
frame.image.dispose();
masked.dispose();
}
} catch (_) {
// Capture failed or timed out (e.g. the engine never committed a
// frame while backgrounded). Reuse the last good frame; the finally
// releases the in-flight guard so capture self-heals next tick.
return _lastCapture;
} finally {
_captureInFlight--;
}
default:
throw PlatformException(
code: 'unimplemented',
message: 'Method ${call.method} not implemented',
);
}
});
}