start method

Future<void> start({
  1. required SendPayload send,
  2. required SnapshotFactory captureSnapshot,
  3. void onSnapshotCaptured(
    1. UiSnapshot snapshot
    )?,
  4. bool canTransmitSnapshot(
    1. UiSnapshot snapshot
    )?,
  5. bool canObserveSnapshot(
    1. UiSnapshot snapshot
    )?,
  6. bool isPayloadAllowed(
    1. Uint8List bytes
    )?,
  7. bool Function() createPayloadGuard(
    1. Uint8List bytes
    )?,
  8. bool Function() createDecodedPayloadGuard(
    1. String type,
    2. Map<String, Object?> payload
    )?,
  9. Uint8List? substituteRejectedPayload(
    1. String type,
    2. Uint8List bytes,
    3. String reason
    )?,
  10. Map<String, Object?> snapshotEnvelope()?,
  11. ScrollerResolver? scrollerResolver,
  12. HighlightCallback? highlightCallback,
  13. BackHandler? onBack,
  14. HighlightDispatcher? standaloneHighlightDispatcher,
  15. MissionStatusListener? onMissionStatus,
  16. MissionOutcomeListener? onMissionTelemetry,
  17. StepOutcomeListener? onStepTelemetry,
  18. NearMissListener? onNearMiss,
  19. bool enableSnapshotPump = true,
  20. bool isReady()?,
})

Wire the runtime to a LiveKit-bound send callback and a snapshot factory.

Implementation

Future<void> start({
  required SendPayload send,
  required SnapshotFactory captureSnapshot,
  void Function(UiSnapshot snapshot)? onSnapshotCaptured,
  bool Function(UiSnapshot snapshot)? canTransmitSnapshot,
  bool Function(UiSnapshot snapshot)? canObserveSnapshot,
  bool Function(Uint8List bytes)? isPayloadAllowed,
  bool Function() Function(Uint8List bytes)? createPayloadGuard,
  bool Function() Function(String type, Map<String, Object?> payload)?
      createDecodedPayloadGuard,
  Uint8List? Function(String type, Uint8List bytes, String reason)?
      substituteRejectedPayload,
  Map<String, Object?> Function()? snapshotEnvelope,
  ScrollerResolver? scrollerResolver,
  HighlightCallback? highlightCallback,
  BackHandler? onBack,
  HighlightDispatcher? standaloneHighlightDispatcher,
  MissionStatusListener? onMissionStatus,
  MissionOutcomeListener? onMissionTelemetry,
  StepOutcomeListener? onStepTelemetry,
  NearMissListener? onNearMiss,
  bool enableSnapshotPump = true,
  bool Function()? isReady,
}) async {
  _voiceLoop?.dispose();
  _snapshotPump?.dispose();
  _snapshotPump = null;
  _outbound?.dispose();

  final queue = OutboundQueue(
    send: send,
    isReady: isReady,
    canSend: isPayloadAllowed,
    createSendGuard: createPayloadGuard,
    createDecodedSendGuard: createDecodedPayloadGuard,
    substituteOnReject: substituteRejectedPayload,
    // LiveKit's reliable data-packet guidance (15 KiB) applies to every
    // frame, not only trees; producers trim to it (pump budget,
    // ExplorationLog.fitToCap, VoiceLoop.fitResponseFrame) and this is the
    // final backstop.
    maxPayloadBytes: kSnapshotSendCapBytes,
    onDropped: (type) {
      if (type == 'ui_diff') {
        _snapshotPump?.invalidateBaseline();
        _snapshotPump?.schedulePush();
      }
    },
  );
  _outbound = queue;
  // Decode each outbound frame ONCE: the type routes it to a lane and the
  // same map feeds the policy guard (createDecodedPayloadGuard), instead of
  // the guard re-decoding a tree frame of up to the full byte cap.
  Future<void> queuedSend(Uint8List bytes) async {
    String? type;
    Map<String, Object?>? payload;
    try {
      final decoded = jsonDecode(utf8.decode(bytes));
      if (decoded is Map<String, Object?>) {
        payload = decoded;
        final t = decoded['type'];
        if (t is String) type = t;
      }
    } catch (_) {}
    queue.enqueue(type ?? '', bytes, payload: payload);
  }

  if (enableSnapshotPump) {
    _snapshotPump = SnapshotPump(
      captureSnapshot: captureSnapshot,
      send: queuedSend,
      onCaptured: onSnapshotCaptured,
      canTransmit: canTransmitSnapshot,
      envelope: snapshotEnvelope,
    );
  }

  // The audit packet is telemetry: it rides the full SEND gate.
  ExplorationLog.instance.setSender(queuedSend);
  ExplorationLog.instance.setScope(canTransmitSnapshot);

  // Idempotent: a restart without stop() must not register a second copy.
  ActionGates.instance.removeListener(_onGatesChanged);
  ActionGates.instance.addListener(_onGatesChanged);

  // "May we LOOK?" is kept apart from "may we SEND?" (Android SnapshotPump
  // P6.1). A mission the backend dispatched must be able to see the screen
  // whenever the HOST allows it — data capture off (production mode, no
  // telemetry) or a hidden FAB must not blind the runner. Only the host's
  // screen exclusions (and a disabled popup) stop it. The pump retains the
  // raw factory for its independent cache consumer.
  final observe = canObserveSnapshot ?? canTransmitSnapshot;
  UiSnapshot? captureForMission() {
    final snapshot = captureSnapshot();
    if (snapshot == null) return null;
    try {
      return (observe?.call(snapshot) ?? true) ? snapshot : null;
    } catch (_) {
      return null;
    }
  }

  _voiceLoop = VoiceLoop(
    send: queuedSend,
    captureSnapshot: captureForMission,
    scrollerResolver: scrollerResolver,
    highlightCallback: highlightCallback,
    onBack: onBack,
    navStack: navStack,
    standaloneHighlightDispatcher: standaloneHighlightDispatcher,
    onRequestSnapshot: () => _snapshotPump?.forcePush(),
    afterStep: () => _snapshotPump?.schedulePush(),
    onMissionStatus: onMissionStatus,
    onMissionTelemetry: onMissionTelemetry,
    onStepTelemetry: onStepTelemetry,
    onNearMiss: onNearMiss,
    explorer: explorer,
    verifier: verifier,
  );
}