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':
        // Take no 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. The destination is
        // captured once the transition settles.
        if (_isNavigationTransitionActive()) return null;
        // Capture ONLY when the UI is fully settled. While anything is
        // animating (scroll, fling, 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). Answer null instead and let the next capture proceed.
        //
        // No tap exception is needed any more: native takes the tap frame at
        // finger-down, before the ripple starts and before Dart's own gesture
        // callbacks run, so the settled frame this sees IS the frame the user
        // tapped on.
        if (_shouldSkip(
            _captureInFlight, _sinceLastFrame.elapsedMilliseconds)) {
          return null;
        }
        _captureInFlight++;
        try {
          // Stashes the exported mask geometry on the side: the collector
          // runs exactly once per capture, inside the same synchronous
          // slice as the rasterisation kickoff (see
          // FrameSource.walkMaskRects), so the geometry reported alongside
          // the bytes comes from the SAME walk the compositor used — rects
          // and pixels can never describe different frames. The two rect
          // sets differ on purpose: the compositor blacks out the
          // content-granular rects, while the exported geometry is expanded
          // to the enclosing control for tap-marker suppression (see
          // collectCaptureMaskRectsSync).
          var exportedLogicalRects = const <Rect>[];
          final source = FlutterFrameSource.forImplicitView(
            maskCollector: () {
              final collected =
                  SessionReplayMasking.collectCaptureMaskRectsSync();
              exportedLogicalRects = collected.exported;
              return collected.content;
            },
            captureScale: _captureScale,
          );
          if (source == null) return null;
          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 null;
            // Same ratio the compositor used, not PlatformDispatcher's:
            // an app overriding createViewConfiguration makes those differ.
            final dpr = source.devicePixelRatio;
            return MaskedFrameBytes(
              bytes: byteData.buffer.asUint8List(),
              width: masked.width,
              height: masked.height,
              maskRects: MaskedFrameBytes.hostMaskRects(
                exportedLogicalRects,
                isIOS: Platform.isIOS,
                devicePixelRatio: dpr,
              ),
            ).toChannelMap();
          } finally {
            frame.image.dispose();
            masked.dispose();
          }
        } catch (_) {
          // Capture failed, timed out (e.g. the engine never committed a
          // frame while backgrounded), or was rejected by the post-frame
          // settle guard. Answer null; the finally releases the in-flight
          // guard so capture self-heals next tick.
          return null;
        } finally {
          _captureInFlight--;
        }
      default:
        throw PlatformException(
          code: 'unimplemented',
          message: 'Method ${call.method} not implemented',
        );
    }
  });
}