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',
        );
    }
  });
}